diff --git a/pkg/server/api/compliancepage/markdown_handler.go b/pkg/server/api/compliancepage/markdown_handler.go new file mode 100644 index 000000000..d0ad1e845 --- /dev/null +++ b/pkg/server/api/compliancepage/markdown_handler.go @@ -0,0 +1,43 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package compliancepage + +import ( + "net/http" + + "go.probo.inc/probo/pkg/trust" +) + +type Handler struct { + trustService *trust.Service +} + +func NewHandler(trustService *trust.Service) *Handler { + return &Handler{trustService: trustService} +} + +func (h *Handler) HandleLLMsTxt(w http.ResponseWriter, r *http.Request) { + tc := CompliancePageFromContext(r.Context()) + if tc == nil { + http.Error(w, "not found", http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + + if err := h.trustService.RenderCompliancePageMarkdown(r.Context(), w, tc.ID, tc.TenantID); err != nil { + http.Error(w, "internal server error", http.StatusInternalServerError) + } +} diff --git a/pkg/server/server.go b/pkg/server/server.go index ed14cab44..3f02a6097 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -174,7 +174,10 @@ func (s *Server) stripTrustPrefix(next http.Handler) http.Handler { func (s *Server) trustCenterRouter() chi.Router { r := chi.NewRouter() + h := compliancepage.NewHandler(s.trustService) + r.Mount("/api/trust/v1", s.apiServer.CompliancePageHandler()) + r.Get("/llms.txt", h.HandleLLMsTxt) r.Handle("/*", s.trustWebServer) return r diff --git a/pkg/trust/compliance.md.tmpl b/pkg/trust/compliance.md.tmpl new file mode 100644 index 000000000..1f649a0a2 --- /dev/null +++ b/pkg/trust/compliance.md.tmpl @@ -0,0 +1,40 @@ +# {{ .OrgName }} — Compliance +{{ if .Description }} +{{ .Description }} +{{ end }}{{ if .Details }} +{{ range .Details }}- **{{ .Label }}**: {{ .Value }} +{{ end }}{{ end }}{{ if .Frameworks }} +## Compliance Frameworks + +{{ range .Frameworks }}- {{ .Name }}{{ if .Description }}: {{ .Description }}{{ end }} +{{ end }}{{ end }}{{ if .Documents }} +## Documents + +| Title | Type | +|-------|------| +{{ range .Documents }}| {{ .Title }} | {{ .Type }} | +{{ end }}{{ end }}{{ if .Audits }} +## Audits + +| Name | Framework | Valid From | Valid Until | +|------|-----------|------------|-------------| +{{ range .Audits }}| {{ .Name }} | {{ .Framework }} | {{ .ValidFrom }} | {{ .ValidUntil }} | +{{ end }}{{ end }}{{ if .Vendors }} +## Subprocessors + +| Name | Category | Countries | Website | +|------|----------|-----------|---------| +{{ range .Vendors }}| {{ .Name }} | {{ .Category }} | {{ .Countries }} | {{ .Website }} | +{{ end }}{{ end }}{{ if .References }} +## References + +| Name | Description | Website | +|------|-------------|---------| +{{ range .References }}| {{ .Name }} | {{ .Description }} | {{ .Website }} | +{{ end }}{{ end }}{{ if .ExternalLinks }} +## External Links + +| Name | URL | +|------|-----| +{{ range .ExternalLinks }}| {{ .Name }} | {{ .URL }} | +{{ end }}{{ end }} \ No newline at end of file diff --git a/pkg/trust/compliance_page_service.go b/pkg/trust/compliance_page_service.go new file mode 100644 index 000000000..8d82427ff --- /dev/null +++ b/pkg/trust/compliance_page_service.go @@ -0,0 +1,417 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package trust + +import ( + "context" + _ "embed" + "fmt" + "io" + "strings" + "text/template" + + "go.gearno.de/x/ref" + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/gid" + "go.probo.inc/probo/pkg/page" +) + +//go:embed compliance.md.tmpl +var complianceTmplContent string + +var complianceTmpl = template.Must(template.New("compliance").Parse(complianceTmplContent)) + +type ( + compliancePageData struct { + OrgName string + Description string + Details []compliancePageDetail + Frameworks []compliancePageFramework + Documents []compliancePageDocument + Audits []compliancePageAudit + Vendors []compliancePageVendor + References []compliancePageReference + ExternalLinks []compliancePageExternalLink + } + + compliancePageDetail struct { + Label string + Value string + } + + compliancePageFramework struct { + Name string + Description string + } + + compliancePageDocument struct { + Title string + Type string + } + + compliancePageAudit struct { + Name string + Framework string + ValidFrom string + ValidUntil string + } + + compliancePageVendor struct { + Name string + Category string + Countries string + Website string + } + + compliancePageReference struct { + Name string + Description string + Website string + } + + compliancePageExternalLink struct { + Name string + URL string + } +) + +func (s *Service) RenderCompliancePageMarkdown( + ctx context.Context, + w io.Writer, + trustCenterID gid.GID, + tenantID gid.TenantID, +) error { + org, err := s.GetOrganizationByTrustCenterID(ctx, trustCenterID) + if err != nil { + return fmt.Errorf("cannot load organization for compliance page: %w", err) + } + + tenantSvc := s.WithTenant(tenantID) + + data := &compliancePageData{ + OrgName: org.Name, + } + + if org.Description != nil && *org.Description != "" { + data.Description = *org.Description + } + + if org.WebsiteURL != nil && *org.WebsiteURL != "" { + data.Details = append(data.Details, compliancePageDetail{Label: "Website", Value: *org.WebsiteURL}) + } + if org.Email != nil && *org.Email != "" { + data.Details = append(data.Details, compliancePageDetail{Label: "Email", Value: *org.Email}) + } + if org.HeadquarterAddress != nil && *org.HeadquarterAddress != "" { + data.Details = append(data.Details, compliancePageDetail{Label: "Headquarters", Value: *org.HeadquarterAddress}) + } + + data.Frameworks = s.fetchComplianceFrameworks(ctx, tenantSvc, trustCenterID) + data.Documents = s.fetchDocuments(ctx, tenantSvc, org.ID) + data.Audits = s.fetchAudits(ctx, tenantSvc, org.ID) + data.Vendors = s.fetchVendors(ctx, tenantSvc, org.ID) + data.References = s.fetchReferences(ctx, tenantSvc, trustCenterID) + data.ExternalLinks = s.fetchExternalLinks(ctx, tenantSvc, trustCenterID) + + if err := complianceTmpl.Execute(w, data); err != nil { + return fmt.Errorf("cannot render compliance page markdown: %w", err) + } + + return nil +} + +func (s *Service) fetchComplianceFrameworks(ctx context.Context, tenantSvc *TenantService, trustCenterID gid.GID) []compliancePageFramework { + var frameworks []compliancePageFramework + + var cursorKey *page.CursorKey + for { + cursor := page.NewCursor( + page.MaxCursorSize, + cursorKey, + page.Head, + page.OrderBy[coredata.ComplianceFrameworkOrderField]{ + Field: coredata.ComplianceFrameworkOrderFieldRank, + Direction: page.OrderDirectionAsc, + }, + ) + + result, err := tenantSvc.ComplianceFrameworks.ListByTrustCenterID(ctx, trustCenterID, cursor) + if err != nil { + break + } + + for _, cf := range result.Data { + if cf.Visibility != coredata.ComplianceFrameworkVisibilityPublic { + continue + } + + fw, err := tenantSvc.Frameworks.Get(ctx, cf.FrameworkID) + if err != nil { + continue + } + + fi := compliancePageFramework{Name: fw.Name} + if fw.Description != nil { + fi.Description = *fw.Description + } + frameworks = append(frameworks, fi) + } + + if !result.Info.HasNext { + break + } + + last := result.Data[len(result.Data)-1] + ck := last.CursorKey(coredata.ComplianceFrameworkOrderFieldRank) + cursorKey = &ck + } + + return frameworks +} + +func (s *Service) fetchDocuments(ctx context.Context, tenantSvc *TenantService, orgID gid.GID) []compliancePageDocument { + var docs []compliancePageDocument + + var cursorKey *page.CursorKey + for { + cursor := page.NewCursor( + page.MaxCursorSize, + cursorKey, + page.Head, + page.OrderBy[coredata.DocumentOrderField]{ + Field: coredata.DocumentOrderFieldTitle, + Direction: page.OrderDirectionAsc, + }, + ) + + result, err := tenantSvc.Documents.ListForOrganizationId(ctx, orgID, cursor) + if err != nil { + break + } + + for _, doc := range result.Data { + if doc.TrustCenterVisibility != coredata.TrustCenterVisibilityPublic { + continue + } + docs = append( + docs, + compliancePageDocument{ + Title: doc.Title, + Type: doc.DocumentType.String(), + }, + ) + } + + if !result.Info.HasNext { + break + } + + last := result.Data[len(result.Data)-1] + ck := last.CursorKey(coredata.DocumentOrderFieldTitle) + cursorKey = &ck + } + + return docs +} + +func (s *Service) fetchAudits(ctx context.Context, tenantSvc *TenantService, orgID gid.GID) []compliancePageAudit { + var audits []compliancePageAudit + + var cursorKey *page.CursorKey + for { + cursor := page.NewCursor( + page.MaxCursorSize, + cursorKey, + page.Head, + page.OrderBy[coredata.AuditOrderField]{ + Field: coredata.AuditOrderFieldCreatedAt, + Direction: page.OrderDirectionAsc, + }, + ) + + result, err := tenantSvc.Audits.ListForOrganizationId(ctx, orgID, cursor) + if err != nil { + break + } + + for _, audit := range result.Data { + if audit.TrustCenterVisibility != coredata.TrustCenterVisibilityPublic { + continue + } + + frameworkName := "" + fw, err := tenantSvc.Frameworks.Get(ctx, audit.FrameworkID) + if err == nil { + frameworkName = fw.Name + } + + ai := compliancePageAudit{ + Name: ref.UnrefOrZero(audit.Name), + Framework: frameworkName, + } + if audit.ValidFrom != nil { + ai.ValidFrom = audit.ValidFrom.Format("2006-01-02") + } + if audit.ValidUntil != nil { + ai.ValidUntil = audit.ValidUntil.Format("2006-01-02") + } + audits = append(audits, ai) + } + + if !result.Info.HasNext { + break + } + + last := result.Data[len(result.Data)-1] + ck := last.CursorKey(coredata.AuditOrderFieldCreatedAt) + cursorKey = &ck + } + + return audits +} + +func (s *Service) fetchVendors(ctx context.Context, tenantSvc *TenantService, orgID gid.GID) []compliancePageVendor { + var vendors []compliancePageVendor + + var cursorKey *page.CursorKey + for { + cursor := page.NewCursor( + page.MaxCursorSize, + cursorKey, + page.Head, + page.OrderBy[coredata.VendorOrderField]{ + Field: coredata.VendorOrderFieldName, + Direction: page.OrderDirectionAsc, + }, + ) + + result, err := tenantSvc.Vendors.ListForOrganizationId(ctx, orgID, cursor) + if err != nil { + break + } + + for _, v := range result.Data { + var countries []string + for _, c := range v.Countries { + countries = append(countries, c.String()) + } + + vendors = append( + vendors, + compliancePageVendor{ + Name: v.Name, + Category: v.Category.String(), + Countries: strings.Join(countries, ", "), + Website: ref.UnrefOrZero(v.WebsiteURL), + }, + ) + } + + if !result.Info.HasNext { + break + } + + last := result.Data[len(result.Data)-1] + ck := last.CursorKey(coredata.VendorOrderFieldName) + cursorKey = &ck + } + + return vendors +} + +func (s *Service) fetchReferences(ctx context.Context, tenantSvc *TenantService, trustCenterID gid.GID) []compliancePageReference { + var refs []compliancePageReference + + var cursorKey *page.CursorKey + for { + cursor := page.NewCursor( + page.MaxCursorSize, + cursorKey, + page.Head, + page.OrderBy[coredata.TrustCenterReferenceOrderField]{ + Field: coredata.TrustCenterReferenceOrderFieldRank, + Direction: page.OrderDirectionAsc, + }, + ) + + result, err := tenantSvc.TrustCenterReferences.ListForTrustCenterID(ctx, trustCenterID, cursor) + if err != nil { + break + } + + for _, r := range result.Data { + ri := compliancePageReference{ + Name: r.Name, + Website: r.WebsiteURL, + } + if r.Description != nil { + ri.Description = *r.Description + } + refs = append(refs, ri) + } + + if !result.Info.HasNext { + break + } + + last := result.Data[len(result.Data)-1] + ck := last.CursorKey(coredata.TrustCenterReferenceOrderFieldRank) + cursorKey = &ck + } + + return refs +} + +func (s *Service) fetchExternalLinks(ctx context.Context, tenantSvc *TenantService, trustCenterID gid.GID) []compliancePageExternalLink { + var links []compliancePageExternalLink + + var cursorKey *page.CursorKey + for { + cursor := page.NewCursor( + page.MaxCursorSize, + cursorKey, + page.Head, + page.OrderBy[coredata.ComplianceExternalURLOrderField]{ + Field: coredata.ComplianceExternalURLOrderFieldRank, + Direction: page.OrderDirectionAsc, + }, + ) + + result, err := tenantSvc.ComplianceExternalURLs.ListForTrustCenterID(ctx, trustCenterID, cursor) + if err != nil { + break + } + + for _, l := range result.Data { + links = append( + links, + compliancePageExternalLink{ + Name: l.Name, + URL: l.URL, + }, + ) + } + + if !result.Info.HasNext { + break + } + + last := result.Data[len(result.Data)-1] + ck := last.CursorKey(coredata.ComplianceExternalURLOrderFieldRank) + cursorKey = &ck + } + + return links +}