From c41d7439d15196bd9180ff3c1634ee572217a32d Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Wed, 4 Jun 2025 18:44:18 -0700 Subject: [PATCH] Add SOA generator Signed-off-by: Bryan Frimin --- .../__generated__/PeopleListQuery.graphql.ts | 4 +- .../frameworks/FrameworkDetailPage.tsx | 50 + ...orkStateOfApplicabilityMutation.graphql.ts | 95 ++ go.mod | 6 + go.sum | 15 + pkg/probo/framework_service.go | 1033 ++++++++++++----- pkg/server/api/console/v1/resolver.go | 3 +- pkg/server/api/console/v1/schema.graphql | 11 + pkg/server/api/console/v1/schema/schema.go | 381 +++++- pkg/server/api/console/v1/types/types.go | 8 + pkg/server/api/console/v1/v1_resolver.go | 14 + 11 files changed, 1280 insertions(+), 340 deletions(-) create mode 100644 apps/console/src/pages/organizations/frameworks/__generated__/FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation.graphql.ts diff --git a/apps/console/src/hooks/graph/__generated__/PeopleListQuery.graphql.ts b/apps/console/src/hooks/graph/__generated__/PeopleListQuery.graphql.ts index e720c10dc..880870f18 100644 --- a/apps/console/src/hooks/graph/__generated__/PeopleListQuery.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/PeopleListQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -11,7 +11,7 @@ import { ConcreteRequest } from 'relay-runtime'; import { FragmentRefs } from "relay-runtime"; export type OrderDirection = "ASC" | "DESC"; -export type PeopleOrderField = "CREATED_AT" | "FULL_NAME"; +export type PeopleOrderField = "CREATED_AT" | "FULL_NAME" | "KIND"; export type PeopleOrder = { direction: OrderDirection; field: PeopleOrderField; diff --git a/apps/console/src/pages/organizations/frameworks/FrameworkDetailPage.tsx b/apps/console/src/pages/organizations/frameworks/FrameworkDetailPage.tsx index 582bb17ed..0c368f6a8 100644 --- a/apps/console/src/pages/organizations/frameworks/FrameworkDetailPage.tsx +++ b/apps/console/src/pages/organizations/frameworks/FrameworkDetailPage.tsx @@ -26,8 +26,13 @@ import { Navigate, Outlet, useNavigate, useParams } from "react-router"; import { useOrganizationId } from "/hooks/useOrganizationId"; import type { FrameworkGraphNodeQuery } from "/hooks/graph/__generated__/FrameworkGraphNodeQuery.graphql"; import type { FrameworkDetailPageFragment$key } from "./__generated__/FrameworkDetailPageFragment.graphql"; +import type { + FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation, + FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation$data, +} from "./__generated__/FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation.graphql"; import { FrameworkFormDialog } from "./dialogs/FrameworkFormDialog"; import { FrameworkControlDialog } from "./dialogs/FrameworkControlDialog"; +import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; const frameworkDetailFragment = graphql` fragment FrameworkDetailPageFragment on Framework { @@ -47,6 +52,18 @@ const frameworkDetailFragment = graphql` } `; +const generateFrameworkStateOfApplicabilityMutation = graphql` + mutation FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation( + $frameworkId: ID! + ) { + generateFrameworkStateOfApplicability( + input: { frameworkId: $frameworkId } + ) { + downloadUrl + } + } +`; + type Props = { queryRef: PreloadedQuery; }; @@ -71,6 +88,16 @@ export default function FrameworkDetailPage(props: Props) { ConnectionHandler.getConnectionID(organizationId, connectionListKey)! ); + const [generateFrameworkStateOfApplicability] = + useMutationWithToasts( + generateFrameworkStateOfApplicabilityMutation, + { + errorMessage: "Failed to generate framework state of applicability", + successMessage: + "Framework state of applicability generated successfully", + } + ); + usePageTitle(`${framework.name} | ${selectedControl?.sectionTitle}`); const onDelete = () => { deleteFramework({ @@ -107,6 +134,29 @@ export default function FrameworkDetailPage(props: Props) { + { + generateFrameworkStateOfApplicability({ + variables: { frameworkId: framework.id }, + onCompleted: ( + data: FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation$data + ) => { + if (data.generateFrameworkStateOfApplicability.downloadUrl) { + const link = document.createElement("a"); + link.href = + data.generateFrameworkStateOfApplicability.downloadUrl; + link.download = `${framework.name}-SOA.pdf`; // You can adjust the filename as needed + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + }, + }); + }} + > + {__("Download SOA")} + {__("Delete")} diff --git a/apps/console/src/pages/organizations/frameworks/__generated__/FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation.graphql.ts b/apps/console/src/pages/organizations/frameworks/__generated__/FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation.graphql.ts new file mode 100644 index 000000000..0f39130a3 --- /dev/null +++ b/apps/console/src/pages/organizations/frameworks/__generated__/FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation.graphql.ts @@ -0,0 +1,95 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation$variables = { + frameworkId: string; +}; +export type FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation$data = { + readonly generateFrameworkStateOfApplicability: { + readonly downloadUrl: string; + }; +}; +export type FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation = { + response: FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation$data; + variables: FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "frameworkId" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "fields": [ + { + "kind": "Variable", + "name": "frameworkId", + "variableName": "frameworkId" + } + ], + "kind": "ObjectValue", + "name": "input" + } + ], + "concreteType": "GenerateFrameworkStateOfApplicabilityPayload", + "kind": "LinkedField", + "name": "generateFrameworkStateOfApplicability", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "downloadUrl", + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "338cbffe84fb19cdf46c97b739216342", + "id": null, + "metadata": {}, + "name": "FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation", + "operationKind": "mutation", + "text": "mutation FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation(\n $frameworkId: ID!\n) {\n generateFrameworkStateOfApplicability(input: {frameworkId: $frameworkId}) {\n downloadUrl\n }\n}\n" + } +}; +})(); + +(node as any).hash = "f5c0a7d2bebedecc3de7d55d52f06cd8"; + +export default node; diff --git a/go.mod b/go.mod index 48111e7b1..4229694df 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/openai/openai-go v1.1.0 github.com/prometheus/client_golang v1.22.0 github.com/vektah/gqlparser/v2 v2.5.27 + github.com/xuri/excelize/v2 v2.9.1 go.gearno.de/crypto/uuid v0.1.0 go.gearno.de/kit v0.0.0-20250623163305-45b4f6905899 go.gearno.de/x/ref v0.0.0-20240502200927-d74926fcb14c @@ -61,6 +62,8 @@ require ( github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.64.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.4 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sosodev/duration v1.3.1 // indirect @@ -69,8 +72,11 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect + github.com/tiendc/go-deepcopy v1.6.0 // indirect github.com/urfave/cli/v2 v2.27.6 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + github.com/xuri/efp v0.0.1 // indirect + github.com/xuri/nfp v0.0.1 // indirect go.gearno.de/x/panicf v0.1.1 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect diff --git a/go.sum b/go.sum index 8ed847f6f..0db7e3a61 100644 --- a/go.sum +++ b/go.sum @@ -120,6 +120,11 @@ github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQP github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= +github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -150,12 +155,20 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tiendc/go-deepcopy v1.6.0 h1:0UtfV/imoCwlLxVsyfUd4hNHnB3drXsfle+wzSCA5Wo= +github.com/tiendc/go-deepcopy v1.6.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I= github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s= github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= +github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw= +github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s= +github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q= +github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= go.gearno.de/crypto/uuid v0.1.0 h1:94BYg7GYItJ6yYZ1GJayb3VYhI9/FjxuR1nFaduR4hE= go.gearno.de/crypto/uuid v0.1.0/go.mod h1:fnIIvKO9QnsyLO3ZJLJT3r8KZv/p0FOeT5eZKilYWXg= go.gearno.de/kit v0.0.0-20250623163305-45b4f6905899 h1:g44W/Fhm5bW7fhMqr874dJKOuHHsRhv6Tr3h6yHLcqY= @@ -186,6 +199,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= +golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= diff --git a/pkg/probo/framework_service.go b/pkg/probo/framework_service.go index f1dc1f0c1..b0478aaae 100644 --- a/pkg/probo/framework_service.go +++ b/pkg/probo/framework_service.go @@ -20,6 +20,7 @@ import ( "io" "os" "path/filepath" + "strings" "time" "archive/tar" @@ -31,9 +32,16 @@ import ( "github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/page" "github.com/getprobo/probo/pkg/slug" + "github.com/xuri/excelize/v2" "go.gearno.de/kit/pg" ) +const ( + maxControlsLimit = 10000 + maxItemsLimit = 1000 + presignExpiry = 15 * time.Minute +) + type ( FrameworkService struct { svc *TenantService @@ -62,6 +70,18 @@ type ( } `json:"controls"` } } + + soaRowData struct { + control *coredata.Control + applicability string + justificationExclusion string + regulatory string + contractual string + bestPractice string + riskAssessment string + securityMeasures string + isApplicable bool + } ) func (s FrameworkService) Create( @@ -80,22 +100,19 @@ func (s FrameworkService) Create( UpdatedAt: now, } - err := s.svc.pg.WithTx( - ctx, - func(conn pg.Conn) error { - if err := organization.LoadByID(ctx, conn, s.svc.scope, req.OrganizationID); err != nil { - return fmt.Errorf("cannot load organization: %w", err) - } + err := s.svc.pg.WithTx(ctx, func(conn pg.Conn) error { + if err := organization.LoadByID(ctx, conn, s.svc.scope, req.OrganizationID); err != nil { + return fmt.Errorf("cannot load organization: %w", err) + } - framework.OrganizationID = organization.ID + framework.OrganizationID = organization.ID - if err := framework.Insert(ctx, conn, s.svc.scope); err != nil { - return fmt.Errorf("cannot insert framework: %w", err) - } + if err := framework.Insert(ctx, conn, s.svc.scope); err != nil { + return fmt.Errorf("cannot insert framework: %w", err) + } - return nil - }, - ) + return nil + }) if err != nil { return nil, err @@ -110,18 +127,14 @@ func (s FrameworkService) CountForOrganizationID( ) (int, error) { var count int - err := s.svc.pg.WithConn( - ctx, - func(conn pg.Conn) (err error) { - frameworks := &coredata.Frameworks{} - count, err = frameworks.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID) - if err != nil { - return fmt.Errorf("cannot count frameworks: %w", err) - } - - return nil - }, - ) + err := s.svc.pg.WithConn(ctx, func(conn pg.Conn) (err error) { + frameworks := &coredata.Frameworks{} + count, err = frameworks.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID) + if err != nil { + return fmt.Errorf("cannot count frameworks: %w", err) + } + return nil + }) if err != nil { return 0, fmt.Errorf("cannot count frameworks: %w", err) @@ -138,27 +151,24 @@ func (s FrameworkService) ListForOrganizationID( var frameworks coredata.Frameworks organization := &coredata.Organization{} - err := s.svc.pg.WithConn( - ctx, - func(conn pg.Conn) error { - if err := organization.LoadByID(ctx, conn, s.svc.scope, organizationID); err != nil { - return fmt.Errorf("cannot load organization: %w", err) - } + err := s.svc.pg.WithConn(ctx, func(conn pg.Conn) error { + if err := organization.LoadByID(ctx, conn, s.svc.scope, organizationID); err != nil { + return fmt.Errorf("cannot load organization: %w", err) + } - err := frameworks.LoadByOrganizationID( - ctx, - conn, - s.svc.scope, - organization.ID, - cursor, - ) - if err != nil { - return fmt.Errorf("cannot load frameworks: %w", err) - } + err := frameworks.LoadByOrganizationID( + ctx, + conn, + s.svc.scope, + organization.ID, + cursor, + ) + if err != nil { + return fmt.Errorf("cannot load frameworks: %w", err) + } - return nil - }, - ) + return nil + }) if err != nil { return nil, err @@ -173,12 +183,9 @@ func (s FrameworkService) Get( ) (*coredata.Framework, error) { framework := &coredata.Framework{} - err := s.svc.pg.WithConn( - ctx, - func(conn pg.Conn) error { - return framework.LoadByID(ctx, conn, s.svc.scope, frameworkID) - }, - ) + err := s.svc.pg.WithConn(ctx, func(conn pg.Conn) error { + return framework.LoadByID(ctx, conn, s.svc.scope, frameworkID) + }) if err != nil { return nil, err @@ -193,24 +200,21 @@ func (s FrameworkService) Update( ) (*coredata.Framework, error) { framework := &coredata.Framework{ID: req.ID} - err := s.svc.pg.WithTx( - ctx, - func(conn pg.Conn) error { - if err := framework.LoadByID(ctx, conn, s.svc.scope, req.ID); err != nil { - return fmt.Errorf("cannot load framework: %w", err) - } + err := s.svc.pg.WithTx(ctx, func(conn pg.Conn) error { + if err := framework.LoadByID(ctx, conn, s.svc.scope, req.ID); err != nil { + return fmt.Errorf("cannot load framework: %w", err) + } - if req.Name != nil { - framework.Name = *req.Name - } + if req.Name != nil { + framework.Name = *req.Name + } - if req.Description != nil { - framework.Description = *req.Description - } + if req.Description != nil { + framework.Description = *req.Description + } - return framework.Update(ctx, conn, s.svc.scope) - }, - ) + return framework.Update(ctx, conn, s.svc.scope) + }) if err != nil { return nil, err } @@ -224,12 +228,9 @@ func (s FrameworkService) Delete( ) error { framework := &coredata.Framework{} - return s.svc.pg.WithConn( - ctx, - func(conn pg.Conn) error { - return framework.Delete(ctx, conn, s.svc.scope, frameworkID) - }, - ) + return s.svc.pg.WithConn(ctx, func(conn pg.Conn) error { + return framework.Delete(ctx, conn, s.svc.scope, frameworkID) + }) } func (s FrameworkService) Import( @@ -241,50 +242,47 @@ func (s FrameworkService) Import( frameworkID := gid.New(organizationID.TenantID(), coredata.FrameworkEntityType) now := time.Now() - err := s.svc.pg.WithTx( - ctx, - func(tx pg.Conn) error { - organization := &coredata.Organization{} - if err := organization.LoadByID(ctx, tx, s.svc.scope, organizationID); err != nil { - return fmt.Errorf("cannot load organization: %w", err) + err := s.svc.pg.WithTx(ctx, func(tx pg.Conn) error { + organization := &coredata.Organization{} + if err := organization.LoadByID(ctx, tx, s.svc.scope, organizationID); err != nil { + return fmt.Errorf("cannot load organization: %w", err) + } + + framework = &coredata.Framework{ + ID: frameworkID, + OrganizationID: organization.ID, + ReferenceID: req.Framework.ID, + Name: req.Framework.Name, + CreatedAt: now, + UpdatedAt: now, + } + + if err := framework.Insert(ctx, tx, s.svc.scope); err != nil { + return fmt.Errorf("cannot insert framework: %w", err) + } + + for _, control := range req.Framework.Controls { + controlID := gid.New(organization.ID.TenantID(), coredata.ControlEntityType) + + now := time.Now() + control := &coredata.Control{ + ID: controlID, + TenantID: organizationID.TenantID(), + FrameworkID: frameworkID, + SectionTitle: control.ID, + Name: control.Name, + Description: control.Description, + CreatedAt: now, + UpdatedAt: now, } - framework = &coredata.Framework{ - ID: frameworkID, - OrganizationID: organization.ID, - ReferenceID: req.Framework.ID, - Name: req.Framework.Name, - CreatedAt: now, - UpdatedAt: now, + if err := control.Insert(ctx, tx, s.svc.scope); err != nil { + return fmt.Errorf("cannot insert control: %w", err) } + } - if err := framework.Insert(ctx, tx, s.svc.scope); err != nil { - return fmt.Errorf("cannot insert framework: %w", err) - } - - for _, control := range req.Framework.Controls { - controlID := gid.New(organization.ID.TenantID(), coredata.ControlEntityType) - - now := time.Now() - control := &coredata.Control{ - ID: controlID, - TenantID: organizationID.TenantID(), - FrameworkID: frameworkID, - SectionTitle: control.ID, - Name: control.Name, - Description: control.Description, - CreatedAt: now, - UpdatedAt: now, - } - - if err := control.Insert(ctx, tx, s.svc.scope); err != nil { - return fmt.Errorf("cannot insert control: %w", err) - } - } - - return nil - }, - ) + return nil + }) if err != nil { return nil, err @@ -300,173 +298,64 @@ func (s FrameworkService) ExportAudit( var archivePath string var objectKey string - err := s.svc.pg.WithConn( - ctx, - func(conn pg.Conn) error { - framework := &coredata.Framework{} - if err := framework.LoadByID(ctx, conn, s.svc.scope, frameworkID); err != nil { - return fmt.Errorf("cannot load framework: %w", err) + err := s.svc.pg.WithConn(ctx, func(conn pg.Conn) error { + framework := &coredata.Framework{} + if err := framework.LoadByID(ctx, conn, s.svc.scope, frameworkID); err != nil { + return fmt.Errorf("cannot load framework: %w", err) + } + + now := time.Now() + exportDir := filepath.Join(os.TempDir(), "probo-export", framework.Name, now.Format("2006-01-02-15-04-05")) + + if err := os.MkdirAll(exportDir, 0755); err != nil { + return fmt.Errorf("cannot create export directory: %w", err) + } + + controls := coredata.Controls{} + controlsCursor := page.NewCursor( + 0, + nil, + page.Head, + page.OrderBy[coredata.ControlOrderField]{ + Field: coredata.ControlOrderFieldCreatedAt, + Direction: page.OrderDirectionAsc, + }, + ) + + if err := controls.LoadByFrameworkID(ctx, conn, s.svc.scope, frameworkID, controlsCursor, coredata.NewControlFilter(nil)); err != nil { + return fmt.Errorf("cannot load controls: %w", err) + } + + for _, control := range controls { + if err := s.exportControlData(ctx, conn, control, exportDir); err != nil { + return fmt.Errorf("cannot export control data: %w", err) } + } - now := time.Now() - exportDir := filepath.Join(os.TempDir(), "probo-export", framework.Name, now.Format("2006-01-02-15-04-05")) + archivePath = exportDir + ".tar.gz" + if err := createTarGzArchive(exportDir, archivePath); err != nil { + return fmt.Errorf("cannot create archive: %w", err) + } + defer os.Remove(archivePath) - if err := os.MkdirAll(exportDir, 0755); err != nil { - return fmt.Errorf("cannot create export directory: %w", err) - } + file, err := os.Open(archivePath) + if err != nil { + return fmt.Errorf("cannot open archive file: %w", err) + } + defer file.Close() - fmt.Println("Exporting framework", framework.Name, "to", exportDir) + objectKey = fmt.Sprintf("exports/%s/%s.tar.gz", frameworkID, now.Format("2006-01-02-15-04-05")) + _, err = s.svc.s3.PutObject(ctx, &s3.PutObjectInput{ + Bucket: aws.String(s.svc.bucket), + Key: aws.String(objectKey), + Body: file, + }) + if err != nil { + return fmt.Errorf("cannot upload archive to S3: %w", err) + } - cursor := page.NewCursor( - 0, - nil, - page.Head, - page.OrderBy[coredata.ControlOrderField]{ - Field: coredata.ControlOrderFieldCreatedAt, - Direction: page.OrderDirectionAsc, - }, - ) - - controls := coredata.Controls{} - if err := controls.LoadByFrameworkID(ctx, conn, s.svc.scope, frameworkID, cursor, coredata.NewControlFilter(nil)); err != nil { - return fmt.Errorf("cannot load controls: %w", err) - } - - for _, control := range controls { - controlDir := filepath.Join(exportDir, filepath.Base(control.SectionTitle)) - if err := os.MkdirAll(controlDir, 0755); err != nil { - return fmt.Errorf("cannot create control directory: %w", err) - } - - measures := coredata.Measures{} - cursor := page.NewCursor( - 0, - nil, - page.Head, - page.OrderBy[coredata.MeasureOrderField]{ - Field: coredata.MeasureOrderFieldCreatedAt, - Direction: page.OrderDirectionAsc, - }, - ) - - if err := measures.LoadByControlID(ctx, conn, s.svc.scope, control.ID, cursor, coredata.NewMeasureFilter(nil)); err != nil { - return fmt.Errorf("cannot load measures: %w", err) - } - - documents := coredata.Documents{} - - cursor2 := page.NewCursor( - 0, - nil, - page.Head, - page.OrderBy[coredata.DocumentOrderField]{ - Field: coredata.DocumentOrderFieldCreatedAt, - Direction: page.OrderDirectionAsc, - }, - ) - - if err := documents.LoadByControlID(ctx, conn, s.svc.scope, control.ID, cursor2, coredata.NewDocumentFilter(nil)); err != nil { - return fmt.Errorf("cannot load documents: %w", err) - } - - for _, document := range documents { - documentDir := filepath.Join(controlDir, filepath.Base(document.Title)) - if err := os.MkdirAll(documentDir, 0755); err != nil { - return fmt.Errorf("cannot create document directory: %w", err) - } - - version := coredata.DocumentVersion{} - if err := version.LoadLatestVersion(ctx, conn, s.svc.scope, document.ID); err != nil { - return fmt.Errorf("cannot load document version: %w", err) - } - - documentFile := filepath.Join(documentDir, "document.md") - if err := os.WriteFile(documentFile, []byte(version.Content), 0644); err != nil { - return fmt.Errorf("cannot write document file: %w", err) - } - } - - for _, measure := range measures { - measureDir := filepath.Join(controlDir, filepath.Base(measure.Name)) - if err := os.MkdirAll(measureDir, 0755); err != nil { - return fmt.Errorf("cannot create measure directory: %w", err) - } - - evidences := coredata.Evidences{} - evidenceCursor := page.NewCursor( - 0, - nil, - page.Head, - page.OrderBy[coredata.EvidenceOrderField]{ - Field: coredata.EvidenceOrderFieldCreatedAt, - Direction: page.OrderDirectionAsc, - }, - ) - - if err := evidences.LoadByMeasureID(ctx, conn, s.svc.scope, measure.ID, evidenceCursor); err != nil { - return fmt.Errorf("cannot load evidences: %w", err) - } - - for _, evidence := range evidences { - evidenceFile := filepath.Join(measureDir, filepath.Base(evidence.Filename)) - - if evidence.Type == coredata.EvidenceTypeFile && evidence.ObjectKey != "" { - output, err := s.svc.s3.GetObject( - ctx, - &s3.GetObjectInput{ - Bucket: aws.String(s.svc.bucket), - Key: aws.String(evidence.ObjectKey), - }, - ) - if err != nil { - return fmt.Errorf("cannot download evidence file: %w", err) - } - - defer output.Body.Close() - - file, err := os.Create(evidenceFile) - if err != nil { - return fmt.Errorf("cannot create evidence file: %w", err) - } - defer file.Close() - - _, err = io.Copy(file, output.Body) - if err != nil { - return fmt.Errorf("cannot write evidence file: %w", err) - } - } - } - } - } - - archivePath = exportDir + ".tar.gz" - if err := createTarGzArchive(exportDir, archivePath); err != nil { - return fmt.Errorf("cannot create archive: %w", err) - } - defer os.Remove(archivePath) - - file, err := os.Open(archivePath) - if err != nil { - return fmt.Errorf("cannot open archive file: %w", err) - } - defer file.Close() - - objectKey = fmt.Sprintf("exports/%s/%s.tar.gz", frameworkID, now.Format("2006-01-02-15-04-05")) - _, err = s.svc.s3.PutObject( - ctx, - &s3.PutObjectInput{ - Bucket: aws.String(s.svc.bucket), - Key: aws.String(objectKey), - Body: file, - }, - ) - if err != nil { - return fmt.Errorf("cannot upload archive to S3: %w", err) - } - - return nil - }, - ) + return nil + }) if err != nil { return "", err @@ -478,7 +367,7 @@ func (s FrameworkService) ExportAudit( Bucket: aws.String(s.svc.bucket), Key: aws.String(objectKey), }, func(opts *s3.PresignOptions) { - opts.Expires = 15 * time.Minute + opts.Expires = presignExpiry }) if err != nil { return "", fmt.Errorf("cannot generate presigned URL: %w", err) @@ -487,6 +376,146 @@ func (s FrameworkService) ExportAudit( return presignedReq.URL, nil } +func (s FrameworkService) exportControlData(ctx context.Context, conn pg.Conn, control *coredata.Control, exportDir string) error { + controlDir := filepath.Join(exportDir, filepath.Base(control.SectionTitle)) + if err := os.MkdirAll(controlDir, 0755); err != nil { + return fmt.Errorf("cannot create control directory: %w", err) + } + + measures, err := s.loadMeasuresForControl(ctx, conn, control.ID) + if err != nil { + return fmt.Errorf("cannot load measures: %w", err) + } + + documents, err := s.loadDocumentsForControl(ctx, conn, control.ID) + if err != nil { + return fmt.Errorf("cannot load documents: %w", err) + } + + if err := s.exportDocuments(ctx, conn, documents, controlDir); err != nil { + return fmt.Errorf("cannot export documents: %w", err) + } + + if err := s.exportMeasures(ctx, conn, measures, controlDir); err != nil { + return fmt.Errorf("cannot export measures: %w", err) + } + + return nil +} + +func (s FrameworkService) loadMeasuresForControl(ctx context.Context, conn pg.Conn, controlID gid.GID) (coredata.Measures, error) { + measures := coredata.Measures{} + cursor := page.NewCursor( + 0, + nil, + page.Head, + page.OrderBy[coredata.MeasureOrderField]{ + Field: coredata.MeasureOrderFieldCreatedAt, + Direction: page.OrderDirectionAsc, + }, + ) + + err := measures.LoadByControlID(ctx, conn, s.svc.scope, controlID, cursor, coredata.NewMeasureFilter(nil)) + return measures, err +} + +func (s FrameworkService) loadDocumentsForControl(ctx context.Context, conn pg.Conn, controlID gid.GID) (coredata.Documents, error) { + documents := coredata.Documents{} + cursor := page.NewCursor( + 0, + nil, + page.Head, + page.OrderBy[coredata.DocumentOrderField]{ + Field: coredata.DocumentOrderFieldCreatedAt, + Direction: page.OrderDirectionAsc, + }, + ) + + err := documents.LoadByControlID(ctx, conn, s.svc.scope, controlID, cursor, coredata.NewDocumentFilter(nil)) + return documents, err +} + +func (s FrameworkService) exportDocuments(ctx context.Context, conn pg.Conn, documents coredata.Documents, controlDir string) error { + for _, document := range documents { + documentDir := filepath.Join(controlDir, filepath.Base(document.Title)) + if err := os.MkdirAll(documentDir, 0755); err != nil { + return fmt.Errorf("cannot create document directory: %w", err) + } + + version := coredata.DocumentVersion{} + if err := version.LoadLatestVersion(ctx, conn, s.svc.scope, document.ID); err != nil { + return fmt.Errorf("cannot load document version: %w", err) + } + + documentFile := filepath.Join(documentDir, "document.md") + if err := os.WriteFile(documentFile, []byte(version.Content), 0644); err != nil { + return fmt.Errorf("cannot write document file: %w", err) + } + } + return nil +} + +func (s FrameworkService) exportMeasures(ctx context.Context, conn pg.Conn, measures coredata.Measures, controlDir string) error { + for _, measure := range measures { + measureDir := filepath.Join(controlDir, filepath.Base(measure.Name)) + if err := os.MkdirAll(measureDir, 0755); err != nil { + return fmt.Errorf("cannot create measure directory: %w", err) + } + + evidences := coredata.Evidences{} + evidenceCursor := page.NewCursor( + 0, + nil, + page.Head, + page.OrderBy[coredata.EvidenceOrderField]{ + Field: coredata.EvidenceOrderFieldCreatedAt, + Direction: page.OrderDirectionAsc, + }, + ) + + if err := evidences.LoadByMeasureID(ctx, conn, s.svc.scope, measure.ID, evidenceCursor); err != nil { + return fmt.Errorf("cannot load evidences: %w", err) + } + + for _, evidence := range evidences { + if err := s.exportEvidence(ctx, evidence, measureDir); err != nil { + return fmt.Errorf("cannot export evidence: %w", err) + } + } + } + return nil +} + +func (s FrameworkService) exportEvidence(ctx context.Context, evidence *coredata.Evidence, measureDir string) error { + if evidence.Type != coredata.EvidenceTypeFile || evidence.ObjectKey == "" { + return nil + } + + evidenceFile := filepath.Join(measureDir, filepath.Base(evidence.Filename)) + + output, err := s.svc.s3.GetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(s.svc.bucket), + Key: aws.String(evidence.ObjectKey), + }) + if err != nil { + return fmt.Errorf("cannot download evidence file: %w", err) + } + defer output.Body.Close() + + file, err := os.Create(evidenceFile) + if err != nil { + return fmt.Errorf("cannot create evidence file: %w", err) + } + defer file.Close() + + _, err = io.Copy(file, output.Body) + if err != nil { + return fmt.Errorf("cannot write evidence file: %w", err) + } + + return nil +} + func createTarGzArchive(sourceDir, targetFile string) error { tarFile, err := os.Create(targetFile) if err != nil { @@ -538,3 +567,459 @@ func createTarGzArchive(sourceDir, targetFile string) error { return err } + +func (s FrameworkService) StateOfApplicability(ctx context.Context, frameworkID gid.GID) (string, error) { + framework := &coredata.Framework{} + var presignedURL string + + err := s.svc.pg.WithConn(ctx, func(conn pg.Conn) error { + if err := framework.LoadByID(ctx, conn, s.svc.scope, frameworkID); err != nil { + return fmt.Errorf("cannot load framework: %w", err) + } + + controls, err := s.loadAllControlsForFramework(ctx, conn, frameworkID) + if err != nil { + return err + } + + soaData, err := s.buildSOAData(ctx, conn, controls) + if err != nil { + return err + } + + excelFilePath, err := s.createSOAExcelFile(framework, soaData) + if err != nil { + return err + } + defer os.Remove(excelFilePath) // Clean up local file after upload + + // Upload to S3 and get presigned URL + presignedURL, err = s.uploadSOAToS3(ctx, framework, excelFilePath) + if err != nil { + return fmt.Errorf("cannot upload SOA to S3: %w", err) + } + + return nil + }) + + if err != nil { + return "", err + } + + return presignedURL, nil +} + +func (s FrameworkService) loadAllControlsForFramework(ctx context.Context, conn pg.Conn, frameworkID gid.GID) (coredata.Controls, error) { + controls := coredata.Controls{} + controlsCursor := page.NewCursor( + maxControlsLimit, + nil, + page.Head, + page.OrderBy[coredata.ControlOrderField]{ + Field: coredata.ControlOrderFieldSectionTitle, + Direction: page.OrderDirectionAsc, + }, + ) + + if err := controls.LoadByFrameworkID(ctx, conn, s.svc.scope, frameworkID, controlsCursor, coredata.NewControlFilter(nil)); err != nil { + return nil, fmt.Errorf("cannot load controls: %w", err) + } + + return controls, nil +} + +func (s FrameworkService) buildSOAData(ctx context.Context, conn pg.Conn, controls coredata.Controls) ([]soaRowData, error) { + var soaData []soaRowData + + for _, control := range controls { + if control == nil { + continue + } + + rowData, err := s.buildSOARowData(ctx, conn, control) + if err != nil { + return nil, fmt.Errorf("cannot build SOA row data for control %s: %w", control.ID, err) + } + + soaData = append(soaData, rowData) + } + + return soaData, nil +} + +func (s FrameworkService) buildSOARowData(ctx context.Context, conn pg.Conn, control *coredata.Control) (soaRowData, error) { + measures, err := s.loadMeasuresForControl(ctx, conn, control.ID) + if err != nil { + return soaRowData{}, fmt.Errorf("cannot load measures: %w", err) + } + + policies, err := s.loadDocumentsForControl(ctx, conn, control.ID) + if err != nil { + return soaRowData{}, fmt.Errorf("cannot load policies: %w", err) + } + + hasEvidence := false + for _, measure := range measures { + if measure == nil { + continue + } + + evidences := coredata.Evidences{} + evidencesCursor := page.NewCursor( + maxItemsLimit, + nil, + page.Head, + page.OrderBy[coredata.EvidenceOrderField]{ + Field: coredata.EvidenceOrderFieldCreatedAt, + Direction: page.OrderDirectionAsc, + }, + ) + + if err := evidences.LoadByMeasureID(ctx, conn, s.svc.scope, measure.ID, evidencesCursor); err != nil { + continue + } + + if len(evidences) > 0 { + hasEvidence = true + break + } + } + + rowData := soaRowData{ + control: control, + } + + if len(measures) > 0 || len(policies) > 0 { + rowData.applicability = "Yes" + rowData.isApplicable = true + rowData.regulatory = "YES" + rowData.bestPractice = "YES" + + if hasEvidence { + rowData.riskAssessment = "YES" + } + + var measuresList []string + for _, measure := range measures { + if measure != nil { + measuresList = append(measuresList, "• "+measure.Name) + } + } + for _, policy := range policies { + if policy != nil { + measuresList = append(measuresList, "• "+policy.Title) + } + } + rowData.securityMeasures = strings.Join(measuresList, "\n") + } else { + rowData.applicability = "No" + rowData.isApplicable = false + rowData.justificationExclusion = "Not applicable to current business operations" + } + + return rowData, nil +} + +func (s FrameworkService) createSOAExcelFile(framework *coredata.Framework, soaData []soaRowData) (string, error) { + f := excelize.NewFile() + defer func() { + if err := f.Close(); err != nil { + fmt.Printf("Error closing Excel file: %v\n", err) + } + }() + + sheetName := "State of Applicability" + f.SetSheetName("Sheet1", sheetName) + + styles, err := s.createExcelStyles(f) + if err != nil { + return "", fmt.Errorf("cannot create Excel styles: %w", err) + } + + if err := s.setupExcelHeader(f, sheetName, styles); err != nil { + return "", fmt.Errorf("cannot setup Excel header: %w", err) + } + + if err := s.populateExcelData(f, sheetName, soaData, styles); err != nil { + return "", fmt.Errorf("cannot populate Excel data: %w", err) + } + + if err := s.applyExcelFormatting(f, sheetName, len(soaData)); err != nil { + return "", fmt.Errorf("cannot apply Excel formatting: %w", err) + } + + fileName := s.generateSOAFileName(framework) + excelFilePath := filepath.Join("./", fileName) + + if err := f.SaveAs(excelFilePath); err != nil { + return "", fmt.Errorf("cannot save Excel file: %w", err) + } + + return excelFilePath, nil +} + +func (s FrameworkService) createExcelStyles(f *excelize.File) (map[string]int, error) { + styles := make(map[string]int) + + headerStyle, err := f.NewStyle(&excelize.Style{ + Font: &excelize.Font{Bold: true, Size: 10, Color: "#000000"}, + Fill: excelize.Fill{Type: "pattern", Color: []string{"#D9D9D9"}, Pattern: 1}, + Border: []excelize.Border{ + {Type: "left", Color: "#000000", Style: 1}, + {Type: "top", Color: "#000000", Style: 1}, + {Type: "bottom", Color: "#000000", Style: 1}, + {Type: "right", Color: "#000000", Style: 1}, + }, + Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center", WrapText: true}, + }) + if err != nil { + return nil, err + } + styles["header"] = headerStyle + + cellStyle, err := f.NewStyle(&excelize.Style{ + Border: []excelize.Border{ + {Type: "left", Color: "#000000", Style: 1}, + {Type: "top", Color: "#000000", Style: 1}, + {Type: "bottom", Color: "#000000", Style: 1}, + {Type: "right", Color: "#000000", Style: 1}, + }, + Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center", WrapText: true}, + }) + if err != nil { + return nil, err + } + styles["cell"] = cellStyle + + textCellStyle, err := f.NewStyle(&excelize.Style{ + Border: []excelize.Border{ + {Type: "left", Color: "#000000", Style: 1}, + {Type: "top", Color: "#000000", Style: 1}, + {Type: "bottom", Color: "#000000", Style: 1}, + {Type: "right", Color: "#000000", Style: 1}, + }, + Alignment: &excelize.Alignment{Horizontal: "left", Vertical: "center", WrapText: true}, + }) + if err != nil { + return nil, err + } + styles["textCell"] = textCellStyle + + greenApplicabilityStyle, err := f.NewStyle(&excelize.Style{ + Border: []excelize.Border{ + {Type: "left", Color: "#000000", Style: 1}, + {Type: "top", Color: "#000000", Style: 1}, + {Type: "bottom", Color: "#000000", Style: 1}, + {Type: "right", Color: "#000000", Style: 1}, + }, + Fill: excelize.Fill{Type: "pattern", Color: []string{"#90EE90"}, Pattern: 1}, + Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center", WrapText: true}, + }) + if err != nil { + return nil, err + } + styles["greenApplicability"] = greenApplicabilityStyle + + redApplicabilityStyle, err := f.NewStyle(&excelize.Style{ + Border: []excelize.Border{ + {Type: "left", Color: "#000000", Style: 1}, + {Type: "top", Color: "#000000", Style: 1}, + {Type: "bottom", Color: "#000000", Style: 1}, + {Type: "right", Color: "#000000", Style: 1}, + }, + Fill: excelize.Fill{Type: "pattern", Color: []string{"#FFB6C1"}, Pattern: 1}, + Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center", WrapText: true}, + }) + if err != nil { + return nil, err + } + styles["redApplicability"] = redApplicabilityStyle + + return styles, nil +} + +func (s FrameworkService) setupExcelHeader(f *excelize.File, sheetName string, styles map[string]int) error { + // Version, Date, Comment, Author, Approver header + f.SetCellValue(sheetName, "A2", "Version") + f.SetCellValue(sheetName, "C2", "Date") + f.SetCellValue(sheetName, "D2", "Comment") + f.SetCellValue(sheetName, "E2", "Author") + f.SetCellValue(sheetName, "F2", "Approver") + + // Sample data for header + f.SetCellValue(sheetName, "A3", "1.0") + f.SetCellValue(sheetName, "C3", time.Now().Format("01/02/2006")) + f.SetCellValue(sheetName, "D3", "Initial SoA") + f.SetCellValue(sheetName, "E3", "System Admin") + f.SetCellValue(sheetName, "F3", "Security Manager") + + // Apply header styles + for _, cell := range []string{"A2", "C2", "D2", "E2", "F2"} { + f.SetCellStyle(sheetName, cell, cell, styles["header"]) + } + for _, cell := range []string{"A3", "C3", "D3", "E3", "F3"} { + f.SetCellStyle(sheetName, cell, cell, styles["cell"]) + } + + // Main table headers (row 6-7) + f.SetCellValue(sheetName, "A6", "Control") + f.SetCellValue(sheetName, "B6", "Control name") + f.SetCellValue(sheetName, "C6", "Applicability") + f.SetCellValue(sheetName, "D6", "Justification for exclusion") + f.SetCellValue(sheetName, "E6", "Justification for inclusion") + f.SetCellValue(sheetName, "I6", "List of security measure or policy") + + // Sub headers for "Justification for inclusion" + f.SetCellValue(sheetName, "E7", "Regulatory") + f.SetCellValue(sheetName, "F7", "Contractual") + f.SetCellValue(sheetName, "G7", "Best practice") + f.SetCellValue(sheetName, "H7", "Risk assessment") + + // Merge cells for main headers + f.MergeCell(sheetName, "A6", "A7") + f.MergeCell(sheetName, "B6", "B7") + f.MergeCell(sheetName, "C6", "C7") + f.MergeCell(sheetName, "D6", "D7") + f.MergeCell(sheetName, "E6", "H6") + f.MergeCell(sheetName, "I6", "I7") + + // Apply header styles + headerCells := []string{"A6", "A7", "B6", "B7", "C6", "C7", "D6", "D7", "E6", "I6", "I7", "E6", "E7", "F6", "F7", "G6", "G7", "H6", "H7"} + for _, cell := range headerCells { + f.SetCellStyle(sheetName, cell, cell, styles["header"]) + } + + return nil +} + +func (s FrameworkService) populateExcelData(f *excelize.File, sheetName string, soaData []soaRowData, styles map[string]int) error { + currentRow := 8 + + for _, rowData := range soaData { + control := rowData.control + + f.SetCellValue(sheetName, fmt.Sprintf("A%d", currentRow), control.SectionTitle) + f.SetCellValue(sheetName, fmt.Sprintf("B%d", currentRow), control.Name) + f.SetCellValue(sheetName, fmt.Sprintf("C%d", currentRow), rowData.applicability) + f.SetCellValue(sheetName, fmt.Sprintf("D%d", currentRow), rowData.justificationExclusion) + f.SetCellValue(sheetName, fmt.Sprintf("E%d", currentRow), rowData.regulatory) + f.SetCellValue(sheetName, fmt.Sprintf("F%d", currentRow), rowData.contractual) + f.SetCellValue(sheetName, fmt.Sprintf("G%d", currentRow), rowData.bestPractice) + f.SetCellValue(sheetName, fmt.Sprintf("H%d", currentRow), rowData.riskAssessment) + f.SetCellValue(sheetName, fmt.Sprintf("I%d", currentRow), rowData.securityMeasures) + + // Apply appropriate styles + for col := 'A'; col <= 'I'; col++ { + cellRef := fmt.Sprintf("%c%d", col, currentRow) + if col == 'C' { // Applicability column + if rowData.isApplicable { + f.SetCellStyle(sheetName, cellRef, cellRef, styles["greenApplicability"]) + } else { + f.SetCellStyle(sheetName, cellRef, cellRef, styles["redApplicability"]) + } + } else if col >= 'E' && col <= 'H' { // Justification columns + f.SetCellStyle(sheetName, cellRef, cellRef, styles["cell"]) + } else { // Other columns + f.SetCellStyle(sheetName, cellRef, cellRef, styles["textCell"]) + } + } + + currentRow++ + } + + return nil +} + +func (s FrameworkService) applyExcelFormatting(f *excelize.File, sheetName string, dataRowCount int) error { + if dataRowCount == 0 { + return nil + } + + lastRow := 8 + dataRowCount - 1 + + // Add data validation for Applicability column + dvRange := fmt.Sprintf("C8:C%d", lastRow) + dv := excelize.NewDataValidation(true) + dv.Sqref = dvRange + dv.SetDropList([]string{"Yes", "No"}) + dv.SetError(excelize.DataValidationErrorStyleStop, "Invalid Input", "Please select Yes or No from the dropdown list.") + if err := f.AddDataValidation(sheetName, dv); err != nil { + return fmt.Errorf("failed to add data validation: %w", err) + } + + // Add data validation for justification columns + for _, col := range []string{"E", "F", "G", "H"} { + dvRange := fmt.Sprintf("%s8:%s%d", col, col, lastRow) + dv := excelize.NewDataValidation(true) + dv.Sqref = dvRange + dv.SetDropList([]string{"YES", "NO", ""}) + dv.SetError(excelize.DataValidationErrorStyleStop, "Invalid Input", "Please select YES, NO, or leave empty.") + if err := f.AddDataValidation(sheetName, dv); err != nil { + return fmt.Errorf("failed to add data validation for column %s: %w", col, err) + } + } + + // Add auto-filter + filterRange := fmt.Sprintf("A6:I%d", lastRow) + if err := f.AutoFilter(sheetName, filterRange, []excelize.AutoFilterOptions{}); err != nil { + return fmt.Errorf("failed to add auto-filter: %w", err) + } + + // Set column widths + f.SetColWidth(sheetName, "A", "A", 12) + f.SetColWidth(sheetName, "B", "B", 35) + f.SetColWidth(sheetName, "C", "C", 12) + f.SetColWidth(sheetName, "D", "D", 25) + f.SetColWidth(sheetName, "E", "E", 12) + f.SetColWidth(sheetName, "F", "F", 12) + f.SetColWidth(sheetName, "G", "G", 12) + f.SetColWidth(sheetName, "H", "H", 12) + f.SetColWidth(sheetName, "I", "I", 40) + + // Set row heights for better visibility + f.SetRowHeight(sheetName, 6, 30) + f.SetRowHeight(sheetName, 7, 30) + + return nil +} + +func (s FrameworkService) generateSOAFileName(framework *coredata.Framework) string { + now := time.Now() + return fmt.Sprintf("SOA_%s_%s.xlsx", + strings.ReplaceAll(framework.Name, " ", "_"), + now.Format("20060102_150405")) +} + +func (s FrameworkService) uploadSOAToS3(ctx context.Context, framework *coredata.Framework, excelFilePath string) (string, error) { + file, err := os.Open(excelFilePath) + if err != nil { + return "", fmt.Errorf("cannot open Excel file: %w", err) + } + defer file.Close() + + objectKey := fmt.Sprintf("soa/%s/%s", framework.ID, s.generateSOAFileName(framework)) + + // Upload file to S3 + _, err = s.svc.s3.PutObject(ctx, &s3.PutObjectInput{ + Bucket: aws.String(s.svc.bucket), + Key: aws.String(objectKey), + Body: file, + }) + if err != nil { + return "", fmt.Errorf("cannot upload SOA file to S3: %w", err) + } + + // Generate presigned URL + presignClient := s3.NewPresignClient(s.svc.s3) + presignedReq, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(s.svc.bucket), + Key: aws.String(objectKey), + }, func(opts *s3.PresignOptions) { + opts.Expires = presignExpiry + }) + if err != nil { + return "", fmt.Errorf("cannot generate presigned URL: %w", err) + } + + return presignedReq.URL, nil +} diff --git a/pkg/server/api/console/v1/resolver.go b/pkg/server/api/console/v1/resolver.go index a06d20403..5d88fa8a6 100644 --- a/pkg/server/api/console/v1/resolver.go +++ b/pkg/server/api/console/v1/resolver.go @@ -22,6 +22,7 @@ import ( "errors" "fmt" "net/http" + "runtime/debug" "strings" "time" @@ -234,7 +235,7 @@ func graphqlHandler(logger *log.Logger, proboSvc *probo.Service, usrmgrSvc *usrm srv.Use(tracingExtension{}) srv.SetRecoverFunc(func(ctx context.Context, err any) error { logger := httpserver.LoggerFromContext(ctx) - logger.Error("resolver panic", log.Any("error", err)) + logger.Error("resolver panic", log.Any("error", err), log.Any("stack", string(debug.Stack()))) return errors.New("internal server error") }) diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index 36ba657a6..ccf5e55cc 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -1289,6 +1289,9 @@ type Mutation { updateFramework(input: UpdateFrameworkInput!): UpdateFrameworkPayload! importFramework(input: ImportFrameworkInput!): ImportFrameworkPayload! deleteFramework(input: DeleteFrameworkInput!): DeleteFrameworkPayload! + generateFrameworkStateOfApplicability( + input: GenerateFrameworkStateOfApplicabilityInput! + ): GenerateFrameworkStateOfApplicabilityPayload! # Control mutations createControl(input: CreateControlInput!): CreateControlPayload! @@ -1398,6 +1401,14 @@ type Mutation { } # Input Types +input GenerateFrameworkStateOfApplicabilityInput { + frameworkId: ID! +} + +type GenerateFrameworkStateOfApplicabilityPayload { + downloadUrl: String! +} + input CreateOrganizationInput { name: String! } diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index 3e239cac4..3d0d40e8a 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -456,6 +456,10 @@ type ComplexityRoot struct { Changelog func(childComplexity int) int } + GenerateFrameworkStateOfApplicabilityPayload struct { + DownloadURL func(childComplexity int) int + } + ImportFrameworkPayload struct { FrameworkEdge func(childComplexity int) int } @@ -494,69 +498,70 @@ type ComplexityRoot struct { } Mutation struct { - AssessVendor func(childComplexity int, input types.AssessVendorInput) int - AssignTask func(childComplexity int, input types.AssignTaskInput) int - ConfirmEmail func(childComplexity int, input types.ConfirmEmailInput) int - CreateAsset func(childComplexity int, input types.CreateAssetInput) int - CreateControl func(childComplexity int, input types.CreateControlInput) int - CreateControlDocumentMapping func(childComplexity int, input types.CreateControlDocumentMappingInput) int - CreateControlMeasureMapping func(childComplexity int, input types.CreateControlMeasureMappingInput) int - CreateDatum func(childComplexity int, input types.CreateDatumInput) int - CreateDocument func(childComplexity int, input types.CreateDocumentInput) int - CreateDraftDocumentVersion func(childComplexity int, input types.CreateDraftDocumentVersionInput) int - CreateFramework func(childComplexity int, input types.CreateFrameworkInput) int - CreateMeasure func(childComplexity int, input types.CreateMeasureInput) int - CreateOrganization func(childComplexity int, input types.CreateOrganizationInput) int - CreatePeople func(childComplexity int, input types.CreatePeopleInput) int - CreateRisk func(childComplexity int, input types.CreateRiskInput) int - CreateRiskDocumentMapping func(childComplexity int, input types.CreateRiskDocumentMappingInput) int - CreateRiskMeasureMapping func(childComplexity int, input types.CreateRiskMeasureMappingInput) int - CreateTask func(childComplexity int, input types.CreateTaskInput) int - CreateVendor func(childComplexity int, input types.CreateVendorInput) int - CreateVendorRiskAssessment func(childComplexity int, input types.CreateVendorRiskAssessmentInput) int - DeleteAsset func(childComplexity int, input types.DeleteAssetInput) int - DeleteControl func(childComplexity int, input types.DeleteControlInput) int - DeleteControlDocumentMapping func(childComplexity int, input types.DeleteControlDocumentMappingInput) int - DeleteControlMeasureMapping func(childComplexity int, input types.DeleteControlMeasureMappingInput) int - DeleteDatum func(childComplexity int, input types.DeleteDatumInput) int - DeleteDocument func(childComplexity int, input types.DeleteDocumentInput) int - DeleteEvidence func(childComplexity int, input types.DeleteEvidenceInput) int - DeleteFramework func(childComplexity int, input types.DeleteFrameworkInput) int - DeleteMeasure func(childComplexity int, input types.DeleteMeasureInput) int - DeletePeople func(childComplexity int, input types.DeletePeopleInput) int - DeleteRisk func(childComplexity int, input types.DeleteRiskInput) int - DeleteRiskDocumentMapping func(childComplexity int, input types.DeleteRiskDocumentMappingInput) int - DeleteRiskMeasureMapping func(childComplexity int, input types.DeleteRiskMeasureMappingInput) int - DeleteTask func(childComplexity int, input types.DeleteTaskInput) int - DeleteVendor func(childComplexity int, input types.DeleteVendorInput) int - DeleteVendorComplianceReport func(childComplexity int, input types.DeleteVendorComplianceReportInput) int - ExportAudit func(childComplexity int, input types.ExportAuditInput) int - FulfillEvidence func(childComplexity int, input types.FulfillEvidenceInput) int - GenerateDocumentChangelog func(childComplexity int, input types.GenerateDocumentChangelogInput) int - ImportFramework func(childComplexity int, input types.ImportFrameworkInput) int - ImportMeasure func(childComplexity int, input types.ImportMeasureInput) int - InviteUser func(childComplexity int, input types.InviteUserInput) int - PublishDocumentVersion func(childComplexity int, input types.PublishDocumentVersionInput) int - RemoveUser func(childComplexity int, input types.RemoveUserInput) int - RequestEvidence func(childComplexity int, input types.RequestEvidenceInput) int - RequestSignature func(childComplexity int, input types.RequestSignatureInput) int - SendSigningNotifications func(childComplexity int, input types.SendSigningNotificationsInput) int - UnassignTask func(childComplexity int, input types.UnassignTaskInput) int - UpdateAsset func(childComplexity int, input types.UpdateAssetInput) int - UpdateControl func(childComplexity int, input types.UpdateControlInput) int - UpdateDatum func(childComplexity int, input types.UpdateDatumInput) int - UpdateDocument func(childComplexity int, input types.UpdateDocumentInput) int - UpdateDocumentVersion func(childComplexity int, input types.UpdateDocumentVersionInput) int - UpdateFramework func(childComplexity int, input types.UpdateFrameworkInput) int - UpdateMeasure func(childComplexity int, input types.UpdateMeasureInput) int - UpdateOrganization func(childComplexity int, input types.UpdateOrganizationInput) int - UpdatePeople func(childComplexity int, input types.UpdatePeopleInput) int - UpdateRisk func(childComplexity int, input types.UpdateRiskInput) int - UpdateTask func(childComplexity int, input types.UpdateTaskInput) int - UpdateVendor func(childComplexity int, input types.UpdateVendorInput) int - UploadMeasureEvidence func(childComplexity int, input types.UploadMeasureEvidenceInput) int - UploadTaskEvidence func(childComplexity int, input types.UploadTaskEvidenceInput) int - UploadVendorComplianceReport func(childComplexity int, input types.UploadVendorComplianceReportInput) int + AssessVendor func(childComplexity int, input types.AssessVendorInput) int + AssignTask func(childComplexity int, input types.AssignTaskInput) int + ConfirmEmail func(childComplexity int, input types.ConfirmEmailInput) int + CreateAsset func(childComplexity int, input types.CreateAssetInput) int + CreateControl func(childComplexity int, input types.CreateControlInput) int + CreateControlDocumentMapping func(childComplexity int, input types.CreateControlDocumentMappingInput) int + CreateControlMeasureMapping func(childComplexity int, input types.CreateControlMeasureMappingInput) int + CreateDatum func(childComplexity int, input types.CreateDatumInput) int + CreateDocument func(childComplexity int, input types.CreateDocumentInput) int + CreateDraftDocumentVersion func(childComplexity int, input types.CreateDraftDocumentVersionInput) int + CreateFramework func(childComplexity int, input types.CreateFrameworkInput) int + CreateMeasure func(childComplexity int, input types.CreateMeasureInput) int + CreateOrganization func(childComplexity int, input types.CreateOrganizationInput) int + CreatePeople func(childComplexity int, input types.CreatePeopleInput) int + CreateRisk func(childComplexity int, input types.CreateRiskInput) int + CreateRiskDocumentMapping func(childComplexity int, input types.CreateRiskDocumentMappingInput) int + CreateRiskMeasureMapping func(childComplexity int, input types.CreateRiskMeasureMappingInput) int + CreateTask func(childComplexity int, input types.CreateTaskInput) int + CreateVendor func(childComplexity int, input types.CreateVendorInput) int + CreateVendorRiskAssessment func(childComplexity int, input types.CreateVendorRiskAssessmentInput) int + DeleteAsset func(childComplexity int, input types.DeleteAssetInput) int + DeleteControl func(childComplexity int, input types.DeleteControlInput) int + DeleteControlDocumentMapping func(childComplexity int, input types.DeleteControlDocumentMappingInput) int + DeleteControlMeasureMapping func(childComplexity int, input types.DeleteControlMeasureMappingInput) int + DeleteDatum func(childComplexity int, input types.DeleteDatumInput) int + DeleteDocument func(childComplexity int, input types.DeleteDocumentInput) int + DeleteEvidence func(childComplexity int, input types.DeleteEvidenceInput) int + DeleteFramework func(childComplexity int, input types.DeleteFrameworkInput) int + DeleteMeasure func(childComplexity int, input types.DeleteMeasureInput) int + DeletePeople func(childComplexity int, input types.DeletePeopleInput) int + DeleteRisk func(childComplexity int, input types.DeleteRiskInput) int + DeleteRiskDocumentMapping func(childComplexity int, input types.DeleteRiskDocumentMappingInput) int + DeleteRiskMeasureMapping func(childComplexity int, input types.DeleteRiskMeasureMappingInput) int + DeleteTask func(childComplexity int, input types.DeleteTaskInput) int + DeleteVendor func(childComplexity int, input types.DeleteVendorInput) int + DeleteVendorComplianceReport func(childComplexity int, input types.DeleteVendorComplianceReportInput) int + ExportAudit func(childComplexity int, input types.ExportAuditInput) int + FulfillEvidence func(childComplexity int, input types.FulfillEvidenceInput) int + GenerateDocumentChangelog func(childComplexity int, input types.GenerateDocumentChangelogInput) int + GenerateFrameworkStateOfApplicability func(childComplexity int, input types.GenerateFrameworkStateOfApplicabilityInput) int + ImportFramework func(childComplexity int, input types.ImportFrameworkInput) int + ImportMeasure func(childComplexity int, input types.ImportMeasureInput) int + InviteUser func(childComplexity int, input types.InviteUserInput) int + PublishDocumentVersion func(childComplexity int, input types.PublishDocumentVersionInput) int + RemoveUser func(childComplexity int, input types.RemoveUserInput) int + RequestEvidence func(childComplexity int, input types.RequestEvidenceInput) int + RequestSignature func(childComplexity int, input types.RequestSignatureInput) int + SendSigningNotifications func(childComplexity int, input types.SendSigningNotificationsInput) int + UnassignTask func(childComplexity int, input types.UnassignTaskInput) int + UpdateAsset func(childComplexity int, input types.UpdateAssetInput) int + UpdateControl func(childComplexity int, input types.UpdateControlInput) int + UpdateDatum func(childComplexity int, input types.UpdateDatumInput) int + UpdateDocument func(childComplexity int, input types.UpdateDocumentInput) int + UpdateDocumentVersion func(childComplexity int, input types.UpdateDocumentVersionInput) int + UpdateFramework func(childComplexity int, input types.UpdateFrameworkInput) int + UpdateMeasure func(childComplexity int, input types.UpdateMeasureInput) int + UpdateOrganization func(childComplexity int, input types.UpdateOrganizationInput) int + UpdatePeople func(childComplexity int, input types.UpdatePeopleInput) int + UpdateRisk func(childComplexity int, input types.UpdateRiskInput) int + UpdateTask func(childComplexity int, input types.UpdateTaskInput) int + UpdateVendor func(childComplexity int, input types.UpdateVendorInput) int + UploadMeasureEvidence func(childComplexity int, input types.UploadMeasureEvidenceInput) int + UploadTaskEvidence func(childComplexity int, input types.UploadTaskEvidenceInput) int + UploadVendorComplianceReport func(childComplexity int, input types.UploadVendorComplianceReportInput) int } Organization struct { @@ -974,6 +979,7 @@ type MutationResolver interface { UpdateFramework(ctx context.Context, input types.UpdateFrameworkInput) (*types.UpdateFrameworkPayload, error) ImportFramework(ctx context.Context, input types.ImportFrameworkInput) (*types.ImportFrameworkPayload, error) DeleteFramework(ctx context.Context, input types.DeleteFrameworkInput) (*types.DeleteFrameworkPayload, error) + GenerateFrameworkStateOfApplicability(ctx context.Context, input types.GenerateFrameworkStateOfApplicabilityInput) (*types.GenerateFrameworkStateOfApplicabilityPayload, error) CreateControl(ctx context.Context, input types.CreateControlInput) (*types.CreateControlPayload, error) UpdateControl(ctx context.Context, input types.UpdateControlInput) (*types.UpdateControlPayload, error) DeleteControl(ctx context.Context, input types.DeleteControlInput) (*types.DeleteControlPayload, error) @@ -2389,6 +2395,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.GenerateDocumentChangelogPayload.Changelog(childComplexity), true + case "GenerateFrameworkStateOfApplicabilityPayload.downloadUrl": + if e.complexity.GenerateFrameworkStateOfApplicabilityPayload.DownloadURL == nil { + break + } + + return e.complexity.GenerateFrameworkStateOfApplicabilityPayload.DownloadURL(childComplexity), true + case "ImportFrameworkPayload.frameworkEdge": if e.complexity.ImportFrameworkPayload.FrameworkEdge == nil { break @@ -3010,6 +3023,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Mutation.GenerateDocumentChangelog(childComplexity, args["input"].(types.GenerateDocumentChangelogInput)), true + case "Mutation.generateFrameworkStateOfApplicability": + if e.complexity.Mutation.GenerateFrameworkStateOfApplicability == nil { + break + } + + args, err := ec.field_Mutation_generateFrameworkStateOfApplicability_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.GenerateFrameworkStateOfApplicability(childComplexity, args["input"].(types.GenerateFrameworkStateOfApplicabilityInput)), true + case "Mutation.importFramework": if e.complexity.Mutation.ImportFramework == nil { break @@ -4695,6 +4720,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputFrameworkOrder, ec.unmarshalInputFulfillEvidenceInput, ec.unmarshalInputGenerateDocumentChangelogInput, + ec.unmarshalInputGenerateFrameworkStateOfApplicabilityInput, ec.unmarshalInputImportFrameworkInput, ec.unmarshalInputImportMeasureInput, ec.unmarshalInputInviteUserInput, @@ -6118,6 +6144,9 @@ type Mutation { updateFramework(input: UpdateFrameworkInput!): UpdateFrameworkPayload! importFramework(input: ImportFrameworkInput!): ImportFrameworkPayload! deleteFramework(input: DeleteFrameworkInput!): DeleteFrameworkPayload! + generateFrameworkStateOfApplicability( + input: GenerateFrameworkStateOfApplicabilityInput! + ): GenerateFrameworkStateOfApplicabilityPayload! # Control mutations createControl(input: CreateControlInput!): CreateControlPayload! @@ -6227,6 +6256,14 @@ type Mutation { } # Input Types +input GenerateFrameworkStateOfApplicabilityInput { + frameworkId: ID! +} + +type GenerateFrameworkStateOfApplicabilityPayload { + downloadUrl: String! +} + input CreateOrganizationInput { name: String! } @@ -9273,6 +9310,29 @@ func (ec *executionContext) field_Mutation_generateDocumentChangelog_argsInput( return zeroVal, nil } +func (ec *executionContext) field_Mutation_generateFrameworkStateOfApplicability_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Mutation_generateFrameworkStateOfApplicability_argsInput(ctx, rawArgs) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} +func (ec *executionContext) field_Mutation_generateFrameworkStateOfApplicability_argsInput( + ctx context.Context, + rawArgs map[string]any, +) (types.GenerateFrameworkStateOfApplicabilityInput, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + if tmp, ok := rawArgs["input"]; ok { + return ec.unmarshalNGenerateFrameworkStateOfApplicabilityInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐGenerateFrameworkStateOfApplicabilityInput(ctx, tmp) + } + + var zeroVal types.GenerateFrameworkStateOfApplicabilityInput + return zeroVal, nil +} + func (ec *executionContext) field_Mutation_importFramework_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -20810,6 +20870,50 @@ func (ec *executionContext) fieldContext_GenerateDocumentChangelogPayload_change return fc, nil } +func (ec *executionContext) _GenerateFrameworkStateOfApplicabilityPayload_downloadUrl(ctx context.Context, field graphql.CollectedField, obj *types.GenerateFrameworkStateOfApplicabilityPayload) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_GenerateFrameworkStateOfApplicabilityPayload_downloadUrl(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.DownloadURL, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_GenerateFrameworkStateOfApplicabilityPayload_downloadUrl(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "GenerateFrameworkStateOfApplicabilityPayload", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _ImportFrameworkPayload_frameworkEdge(ctx context.Context, field graphql.CollectedField, obj *types.ImportFrameworkPayload) (ret graphql.Marshaler) { fc, err := ec.fieldContext_ImportFrameworkPayload_frameworkEdge(ctx, field) if err != nil { @@ -22659,6 +22763,65 @@ func (ec *executionContext) fieldContext_Mutation_deleteFramework(ctx context.Co return fc, nil } +func (ec *executionContext) _Mutation_generateFrameworkStateOfApplicability(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_generateFrameworkStateOfApplicability(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().GenerateFrameworkStateOfApplicability(rctx, fc.Args["input"].(types.GenerateFrameworkStateOfApplicabilityInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*types.GenerateFrameworkStateOfApplicabilityPayload) + fc.Result = res + return ec.marshalNGenerateFrameworkStateOfApplicabilityPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐGenerateFrameworkStateOfApplicabilityPayload(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_generateFrameworkStateOfApplicability(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "downloadUrl": + return ec.fieldContext_GenerateFrameworkStateOfApplicabilityPayload_downloadUrl(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type GenerateFrameworkStateOfApplicabilityPayload", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_generateFrameworkStateOfApplicability_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Mutation_createControl(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation_createControl(ctx, field) if err != nil { @@ -38883,6 +39046,33 @@ func (ec *executionContext) unmarshalInputGenerateDocumentChangelogInput(ctx con return it, nil } +func (ec *executionContext) unmarshalInputGenerateFrameworkStateOfApplicabilityInput(ctx context.Context, obj any) (types.GenerateFrameworkStateOfApplicabilityInput, error) { + var it types.GenerateFrameworkStateOfApplicabilityInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"frameworkId"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "frameworkId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("frameworkId")) + data, err := ec.unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v) + if err != nil { + return it, err + } + it.FrameworkID = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputImportFrameworkInput(ctx context.Context, obj any) (types.ImportFrameworkInput, error) { var it types.ImportFrameworkInput asMap := map[string]any{} @@ -44756,6 +44946,45 @@ func (ec *executionContext) _GenerateDocumentChangelogPayload(ctx context.Contex return out } +var generateFrameworkStateOfApplicabilityPayloadImplementors = []string{"GenerateFrameworkStateOfApplicabilityPayload"} + +func (ec *executionContext) _GenerateFrameworkStateOfApplicabilityPayload(ctx context.Context, sel ast.SelectionSet, obj *types.GenerateFrameworkStateOfApplicabilityPayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, generateFrameworkStateOfApplicabilityPayloadImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("GenerateFrameworkStateOfApplicabilityPayload") + case "downloadUrl": + out.Values[i] = ec._GenerateFrameworkStateOfApplicabilityPayload_downloadUrl(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var importFrameworkPayloadImplementors = []string{"ImportFrameworkPayload"} func (ec *executionContext) _ImportFrameworkPayload(ctx context.Context, sel ast.SelectionSet, obj *types.ImportFrameworkPayload) graphql.Marshaler { @@ -45334,6 +45563,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } + case "generateFrameworkStateOfApplicability": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_generateFrameworkStateOfApplicability(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "createControl": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_createControl(ctx, field) @@ -51728,6 +51964,25 @@ func (ec *executionContext) marshalNGenerateDocumentChangelogPayload2ᚖgithub return ec._GenerateDocumentChangelogPayload(ctx, sel, v) } +func (ec *executionContext) unmarshalNGenerateFrameworkStateOfApplicabilityInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐGenerateFrameworkStateOfApplicabilityInput(ctx context.Context, v any) (types.GenerateFrameworkStateOfApplicabilityInput, error) { + res, err := ec.unmarshalInputGenerateFrameworkStateOfApplicabilityInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNGenerateFrameworkStateOfApplicabilityPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐGenerateFrameworkStateOfApplicabilityPayload(ctx context.Context, sel ast.SelectionSet, v types.GenerateFrameworkStateOfApplicabilityPayload) graphql.Marshaler { + return ec._GenerateFrameworkStateOfApplicabilityPayload(ctx, sel, &v) +} + +func (ec *executionContext) marshalNGenerateFrameworkStateOfApplicabilityPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐGenerateFrameworkStateOfApplicabilityPayload(ctx context.Context, sel ast.SelectionSet, v *types.GenerateFrameworkStateOfApplicabilityPayload) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._GenerateFrameworkStateOfApplicabilityPayload(ctx, sel, v) +} + func (ec *executionContext) unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx context.Context, v any) (gid.GID, error) { res, err := types.UnmarshalGIDScalar(v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go index 0ad071888..44a59398d 100644 --- a/pkg/server/api/console/v1/types/types.go +++ b/pkg/server/api/console/v1/types/types.go @@ -654,6 +654,14 @@ type GenerateDocumentChangelogPayload struct { Changelog string `json:"changelog"` } +type GenerateFrameworkStateOfApplicabilityInput struct { + FrameworkID gid.GID `json:"frameworkId"` +} + +type GenerateFrameworkStateOfApplicabilityPayload struct { + DownloadURL string `json:"downloadUrl"` +} + type ImportFrameworkInput struct { OrganizationID gid.GID `json:"organizationId"` File graphql.Upload `json:"file"` diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index a8c976efb..f7a3a979f 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -1178,6 +1178,20 @@ func (r *mutationResolver) DeleteFramework(ctx context.Context, input types.Dele }, nil } +// GenerateFrameworkStateOfApplicability is the resolver for the generateFrameworkStateOfApplicability field. +func (r *mutationResolver) GenerateFrameworkStateOfApplicability(ctx context.Context, input types.GenerateFrameworkStateOfApplicabilityInput) (*types.GenerateFrameworkStateOfApplicabilityPayload, error) { + prb := r.ProboService(ctx, input.FrameworkID.TenantID()) + + soa, err := prb.Frameworks.StateOfApplicability(ctx, input.FrameworkID) + if err != nil { + return nil, fmt.Errorf("cannot generate framework SOA: %w", err) + } + + return &types.GenerateFrameworkStateOfApplicabilityPayload{ + DownloadURL: soa, + }, nil +} + // CreateControl is the resolver for the createControl field. func (r *mutationResolver) CreateControl(ctx context.Context, input types.CreateControlInput) (*types.CreateControlPayload, error) { prb := r.ProboService(ctx, input.FrameworkID.TenantID())