Add SOA generator

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-06-04 18:44:18 -07:00
parent 3d734a08cb
commit c41d7439d1
11 changed files with 1280 additions and 340 deletions

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<bc0fdc02fb8b596386e290ae0cf0b8b8>> * @generated SignedSource<<e7dfc0de7524eaa22ca235ac0b6e3011>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -11,7 +11,7 @@
import { ConcreteRequest } from 'relay-runtime'; import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime"; import { FragmentRefs } from "relay-runtime";
export type OrderDirection = "ASC" | "DESC"; export type OrderDirection = "ASC" | "DESC";
export type PeopleOrderField = "CREATED_AT" | "FULL_NAME"; export type PeopleOrderField = "CREATED_AT" | "FULL_NAME" | "KIND";
export type PeopleOrder = { export type PeopleOrder = {
direction: OrderDirection; direction: OrderDirection;
field: PeopleOrderField; field: PeopleOrderField;

View File

@@ -26,8 +26,13 @@ import { Navigate, Outlet, useNavigate, useParams } from "react-router";
import { useOrganizationId } from "/hooks/useOrganizationId"; import { useOrganizationId } from "/hooks/useOrganizationId";
import type { FrameworkGraphNodeQuery } from "/hooks/graph/__generated__/FrameworkGraphNodeQuery.graphql"; import type { FrameworkGraphNodeQuery } from "/hooks/graph/__generated__/FrameworkGraphNodeQuery.graphql";
import type { FrameworkDetailPageFragment$key } from "./__generated__/FrameworkDetailPageFragment.graphql"; import type { FrameworkDetailPageFragment$key } from "./__generated__/FrameworkDetailPageFragment.graphql";
import type {
FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation,
FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation$data,
} from "./__generated__/FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation.graphql";
import { FrameworkFormDialog } from "./dialogs/FrameworkFormDialog"; import { FrameworkFormDialog } from "./dialogs/FrameworkFormDialog";
import { FrameworkControlDialog } from "./dialogs/FrameworkControlDialog"; import { FrameworkControlDialog } from "./dialogs/FrameworkControlDialog";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
const frameworkDetailFragment = graphql` const frameworkDetailFragment = graphql`
fragment FrameworkDetailPageFragment on Framework { 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 = { type Props = {
queryRef: PreloadedQuery<FrameworkGraphNodeQuery>; queryRef: PreloadedQuery<FrameworkGraphNodeQuery>;
}; };
@@ -71,6 +88,16 @@ export default function FrameworkDetailPage(props: Props) {
ConnectionHandler.getConnectionID(organizationId, connectionListKey)! ConnectionHandler.getConnectionID(organizationId, connectionListKey)!
); );
const [generateFrameworkStateOfApplicability] =
useMutationWithToasts<FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation>(
generateFrameworkStateOfApplicabilityMutation,
{
errorMessage: "Failed to generate framework state of applicability",
successMessage:
"Framework state of applicability generated successfully",
}
);
usePageTitle(`${framework.name} | ${selectedControl?.sectionTitle}`); usePageTitle(`${framework.name} | ${selectedControl?.sectionTitle}`);
const onDelete = () => { const onDelete = () => {
deleteFramework({ deleteFramework({
@@ -107,6 +134,29 @@ export default function FrameworkDetailPage(props: Props) {
</Button> </Button>
</FrameworkFormDialog> </FrameworkFormDialog>
<ActionDropdown variant="secondary"> <ActionDropdown variant="secondary">
<DropdownItem
variant="primary"
onClick={() => {
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")}
</DropdownItem>
<DropdownItem icon={IconTrashCan} variant="danger" onClick={onDelete}> <DropdownItem icon={IconTrashCan} variant="danger" onClick={onDelete}>
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>

View File

@@ -0,0 +1,95 @@
/**
* @generated SignedSource<<bcdaed0c84805f387ee7d1de8ece0068>>
* @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;

6
go.mod
View File

@@ -18,6 +18,7 @@ require (
github.com/openai/openai-go v1.1.0 github.com/openai/openai-go v1.1.0
github.com/prometheus/client_golang v1.22.0 github.com/prometheus/client_golang v1.22.0
github.com/vektah/gqlparser/v2 v2.5.27 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/crypto/uuid v0.1.0
go.gearno.de/kit v0.0.0-20250623163305-45b4f6905899 go.gearno.de/kit v0.0.0-20250623163305-45b4f6905899
go.gearno.de/x/ref v0.0.0-20240502200927-d74926fcb14c 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/client_model v0.6.2 // indirect
github.com/prometheus/common v0.64.0 // indirect github.com/prometheus/common v0.64.0 // indirect
github.com/prometheus/procfs v0.16.1 // 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/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sosodev/duration v1.3.1 // 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/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // 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/urfave/cli/v2 v2.27.6 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // 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.gearno.de/x/panicf v0.1.1 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect

15
go.sum
View File

@@ -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/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 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 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.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 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/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 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= 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 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= 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 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= 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 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 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 h1:94BYg7GYItJ6yYZ1GJayb3VYhI9/FjxuR1nFaduR4hE=
go.gearno.de/crypto/uuid v0.1.0/go.mod h1:fnIIvKO9QnsyLO3ZJLJT3r8KZv/p0FOeT5eZKilYWXg= 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= 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= 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 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 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 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=

View File

@@ -20,6 +20,7 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"time" "time"
"archive/tar" "archive/tar"
@@ -31,9 +32,16 @@ import (
"github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/page" "github.com/getprobo/probo/pkg/page"
"github.com/getprobo/probo/pkg/slug" "github.com/getprobo/probo/pkg/slug"
"github.com/xuri/excelize/v2"
"go.gearno.de/kit/pg" "go.gearno.de/kit/pg"
) )
const (
maxControlsLimit = 10000
maxItemsLimit = 1000
presignExpiry = 15 * time.Minute
)
type ( type (
FrameworkService struct { FrameworkService struct {
svc *TenantService svc *TenantService
@@ -62,6 +70,18 @@ type (
} `json:"controls"` } `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( func (s FrameworkService) Create(
@@ -80,9 +100,7 @@ func (s FrameworkService) Create(
UpdatedAt: now, UpdatedAt: now,
} }
err := s.svc.pg.WithTx( err := s.svc.pg.WithTx(ctx, func(conn pg.Conn) error {
ctx,
func(conn pg.Conn) error {
if err := organization.LoadByID(ctx, conn, s.svc.scope, req.OrganizationID); err != nil { if err := organization.LoadByID(ctx, conn, s.svc.scope, req.OrganizationID); err != nil {
return fmt.Errorf("cannot load organization: %w", err) return fmt.Errorf("cannot load organization: %w", err)
} }
@@ -94,8 +112,7 @@ func (s FrameworkService) Create(
} }
return nil return nil
}, })
)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -110,18 +127,14 @@ func (s FrameworkService) CountForOrganizationID(
) (int, error) { ) (int, error) {
var count int var count int
err := s.svc.pg.WithConn( err := s.svc.pg.WithConn(ctx, func(conn pg.Conn) (err error) {
ctx,
func(conn pg.Conn) (err error) {
frameworks := &coredata.Frameworks{} frameworks := &coredata.Frameworks{}
count, err = frameworks.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID) count, err = frameworks.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID)
if err != nil { if err != nil {
return fmt.Errorf("cannot count frameworks: %w", err) return fmt.Errorf("cannot count frameworks: %w", err)
} }
return nil return nil
}, })
)
if err != nil { if err != nil {
return 0, fmt.Errorf("cannot count frameworks: %w", err) return 0, fmt.Errorf("cannot count frameworks: %w", err)
@@ -138,9 +151,7 @@ func (s FrameworkService) ListForOrganizationID(
var frameworks coredata.Frameworks var frameworks coredata.Frameworks
organization := &coredata.Organization{} organization := &coredata.Organization{}
err := s.svc.pg.WithConn( err := s.svc.pg.WithConn(ctx, func(conn pg.Conn) error {
ctx,
func(conn pg.Conn) error {
if err := organization.LoadByID(ctx, conn, s.svc.scope, organizationID); err != nil { if err := organization.LoadByID(ctx, conn, s.svc.scope, organizationID); err != nil {
return fmt.Errorf("cannot load organization: %w", err) return fmt.Errorf("cannot load organization: %w", err)
} }
@@ -157,8 +168,7 @@ func (s FrameworkService) ListForOrganizationID(
} }
return nil return nil
}, })
)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -173,12 +183,9 @@ func (s FrameworkService) Get(
) (*coredata.Framework, error) { ) (*coredata.Framework, error) {
framework := &coredata.Framework{} framework := &coredata.Framework{}
err := s.svc.pg.WithConn( err := s.svc.pg.WithConn(ctx, func(conn pg.Conn) error {
ctx,
func(conn pg.Conn) error {
return framework.LoadByID(ctx, conn, s.svc.scope, frameworkID) return framework.LoadByID(ctx, conn, s.svc.scope, frameworkID)
}, })
)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -193,9 +200,7 @@ func (s FrameworkService) Update(
) (*coredata.Framework, error) { ) (*coredata.Framework, error) {
framework := &coredata.Framework{ID: req.ID} framework := &coredata.Framework{ID: req.ID}
err := s.svc.pg.WithTx( err := s.svc.pg.WithTx(ctx, func(conn pg.Conn) error {
ctx,
func(conn pg.Conn) error {
if err := framework.LoadByID(ctx, conn, s.svc.scope, req.ID); err != nil { if err := framework.LoadByID(ctx, conn, s.svc.scope, req.ID); err != nil {
return fmt.Errorf("cannot load framework: %w", err) return fmt.Errorf("cannot load framework: %w", err)
} }
@@ -209,8 +214,7 @@ func (s FrameworkService) Update(
} }
return framework.Update(ctx, conn, s.svc.scope) return framework.Update(ctx, conn, s.svc.scope)
}, })
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -224,12 +228,9 @@ func (s FrameworkService) Delete(
) error { ) error {
framework := &coredata.Framework{} framework := &coredata.Framework{}
return s.svc.pg.WithConn( return s.svc.pg.WithConn(ctx, func(conn pg.Conn) error {
ctx,
func(conn pg.Conn) error {
return framework.Delete(ctx, conn, s.svc.scope, frameworkID) return framework.Delete(ctx, conn, s.svc.scope, frameworkID)
}, })
)
} }
func (s FrameworkService) Import( func (s FrameworkService) Import(
@@ -241,9 +242,7 @@ func (s FrameworkService) Import(
frameworkID := gid.New(organizationID.TenantID(), coredata.FrameworkEntityType) frameworkID := gid.New(organizationID.TenantID(), coredata.FrameworkEntityType)
now := time.Now() now := time.Now()
err := s.svc.pg.WithTx( err := s.svc.pg.WithTx(ctx, func(tx pg.Conn) error {
ctx,
func(tx pg.Conn) error {
organization := &coredata.Organization{} organization := &coredata.Organization{}
if err := organization.LoadByID(ctx, tx, s.svc.scope, organizationID); err != nil { if err := organization.LoadByID(ctx, tx, s.svc.scope, organizationID); err != nil {
return fmt.Errorf("cannot load organization: %w", err) return fmt.Errorf("cannot load organization: %w", err)
@@ -283,8 +282,7 @@ func (s FrameworkService) Import(
} }
return nil return nil
}, })
)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -300,9 +298,7 @@ func (s FrameworkService) ExportAudit(
var archivePath string var archivePath string
var objectKey string var objectKey string
err := s.svc.pg.WithConn( err := s.svc.pg.WithConn(ctx, func(conn pg.Conn) error {
ctx,
func(conn pg.Conn) error {
framework := &coredata.Framework{} framework := &coredata.Framework{}
if err := framework.LoadByID(ctx, conn, s.svc.scope, frameworkID); err != nil { if err := framework.LoadByID(ctx, conn, s.svc.scope, frameworkID); err != nil {
return fmt.Errorf("cannot load framework: %w", err) return fmt.Errorf("cannot load framework: %w", err)
@@ -315,9 +311,8 @@ func (s FrameworkService) ExportAudit(
return fmt.Errorf("cannot create export directory: %w", err) return fmt.Errorf("cannot create export directory: %w", err)
} }
fmt.Println("Exporting framework", framework.Name, "to", exportDir) controls := coredata.Controls{}
controlsCursor := page.NewCursor(
cursor := page.NewCursor(
0, 0,
nil, nil,
page.Head, page.Head,
@@ -327,17 +322,88 @@ func (s FrameworkService) ExportAudit(
}, },
) )
controls := coredata.Controls{} if err := controls.LoadByFrameworkID(ctx, conn, s.svc.scope, frameworkID, controlsCursor, coredata.NewControlFilter(nil)); err != nil {
if err := controls.LoadByFrameworkID(ctx, conn, s.svc.scope, frameworkID, cursor, coredata.NewControlFilter(nil)); err != nil {
return fmt.Errorf("cannot load controls: %w", err) return fmt.Errorf("cannot load controls: %w", err)
} }
for _, control := range controls { for _, control := range controls {
if err := s.exportControlData(ctx, conn, control, exportDir); err != nil {
return fmt.Errorf("cannot export control data: %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
})
if err != nil {
return "", err
}
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
}
func (s FrameworkService) exportControlData(ctx context.Context, conn pg.Conn, control *coredata.Control, exportDir string) error {
controlDir := filepath.Join(exportDir, filepath.Base(control.SectionTitle)) controlDir := filepath.Join(exportDir, filepath.Base(control.SectionTitle))
if err := os.MkdirAll(controlDir, 0755); err != nil { if err := os.MkdirAll(controlDir, 0755); err != nil {
return fmt.Errorf("cannot create control directory: %w", err) 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{} measures := coredata.Measures{}
cursor := page.NewCursor( cursor := page.NewCursor(
0, 0,
@@ -349,13 +415,13 @@ func (s FrameworkService) ExportAudit(
}, },
) )
if err := measures.LoadByControlID(ctx, conn, s.svc.scope, control.ID, cursor, coredata.NewMeasureFilter(nil)); err != nil { err := measures.LoadByControlID(ctx, conn, s.svc.scope, controlID, cursor, coredata.NewMeasureFilter(nil))
return fmt.Errorf("cannot load measures: %w", err) return measures, err
} }
func (s FrameworkService) loadDocumentsForControl(ctx context.Context, conn pg.Conn, controlID gid.GID) (coredata.Documents, error) {
documents := coredata.Documents{} documents := coredata.Documents{}
cursor := page.NewCursor(
cursor2 := page.NewCursor(
0, 0,
nil, nil,
page.Head, page.Head,
@@ -365,10 +431,11 @@ func (s FrameworkService) ExportAudit(
}, },
) )
if err := documents.LoadByControlID(ctx, conn, s.svc.scope, control.ID, cursor2, coredata.NewDocumentFilter(nil)); err != nil { err := documents.LoadByControlID(ctx, conn, s.svc.scope, controlID, cursor, coredata.NewDocumentFilter(nil))
return fmt.Errorf("cannot load documents: %w", err) return documents, err
} }
func (s FrameworkService) exportDocuments(ctx context.Context, conn pg.Conn, documents coredata.Documents, controlDir string) error {
for _, document := range documents { for _, document := range documents {
documentDir := filepath.Join(controlDir, filepath.Base(document.Title)) documentDir := filepath.Join(controlDir, filepath.Base(document.Title))
if err := os.MkdirAll(documentDir, 0755); err != nil { if err := os.MkdirAll(documentDir, 0755); err != nil {
@@ -385,7 +452,10 @@ func (s FrameworkService) ExportAudit(
return fmt.Errorf("cannot write document file: %w", err) 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 { for _, measure := range measures {
measureDir := filepath.Join(controlDir, filepath.Base(measure.Name)) measureDir := filepath.Join(controlDir, filepath.Base(measure.Name))
if err := os.MkdirAll(measureDir, 0755); err != nil { if err := os.MkdirAll(measureDir, 0755); err != nil {
@@ -408,20 +478,28 @@ func (s FrameworkService) ExportAudit(
} }
for _, evidence := range evidences { 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)) evidenceFile := filepath.Join(measureDir, filepath.Base(evidence.Filename))
if evidence.Type == coredata.EvidenceTypeFile && evidence.ObjectKey != "" { output, err := s.svc.s3.GetObject(ctx, &s3.GetObjectInput{
output, err := s.svc.s3.GetObject(
ctx,
&s3.GetObjectInput{
Bucket: aws.String(s.svc.bucket), Bucket: aws.String(s.svc.bucket),
Key: aws.String(evidence.ObjectKey), Key: aws.String(evidence.ObjectKey),
}, })
)
if err != nil { if err != nil {
return fmt.Errorf("cannot download evidence file: %w", err) return fmt.Errorf("cannot download evidence file: %w", err)
} }
defer output.Body.Close() defer output.Body.Close()
file, err := os.Create(evidenceFile) file, err := os.Create(evidenceFile)
@@ -434,57 +512,8 @@ func (s FrameworkService) ExportAudit(
if err != nil { if err != nil {
return fmt.Errorf("cannot write evidence file: %w", err) 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
}
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 = 15 * time.Minute
})
if err != nil {
return "", fmt.Errorf("cannot generate presigned URL: %w", err)
}
return presignedReq.URL, nil
} }
func createTarGzArchive(sourceDir, targetFile string) error { func createTarGzArchive(sourceDir, targetFile string) error {
@@ -538,3 +567,459 @@ func createTarGzArchive(sourceDir, targetFile string) error {
return err 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
}

View File

@@ -22,6 +22,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"runtime/debug"
"strings" "strings"
"time" "time"
@@ -234,7 +235,7 @@ func graphqlHandler(logger *log.Logger, proboSvc *probo.Service, usrmgrSvc *usrm
srv.Use(tracingExtension{}) srv.Use(tracingExtension{})
srv.SetRecoverFunc(func(ctx context.Context, err any) error { srv.SetRecoverFunc(func(ctx context.Context, err any) error {
logger := httpserver.LoggerFromContext(ctx) 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") return errors.New("internal server error")
}) })

View File

@@ -1289,6 +1289,9 @@ type Mutation {
updateFramework(input: UpdateFrameworkInput!): UpdateFrameworkPayload! updateFramework(input: UpdateFrameworkInput!): UpdateFrameworkPayload!
importFramework(input: ImportFrameworkInput!): ImportFrameworkPayload! importFramework(input: ImportFrameworkInput!): ImportFrameworkPayload!
deleteFramework(input: DeleteFrameworkInput!): DeleteFrameworkPayload! deleteFramework(input: DeleteFrameworkInput!): DeleteFrameworkPayload!
generateFrameworkStateOfApplicability(
input: GenerateFrameworkStateOfApplicabilityInput!
): GenerateFrameworkStateOfApplicabilityPayload!
# Control mutations # Control mutations
createControl(input: CreateControlInput!): CreateControlPayload! createControl(input: CreateControlInput!): CreateControlPayload!
@@ -1398,6 +1401,14 @@ type Mutation {
} }
# Input Types # Input Types
input GenerateFrameworkStateOfApplicabilityInput {
frameworkId: ID!
}
type GenerateFrameworkStateOfApplicabilityPayload {
downloadUrl: String!
}
input CreateOrganizationInput { input CreateOrganizationInput {
name: String! name: String!
} }

View File

@@ -456,6 +456,10 @@ type ComplexityRoot struct {
Changelog func(childComplexity int) int Changelog func(childComplexity int) int
} }
GenerateFrameworkStateOfApplicabilityPayload struct {
DownloadURL func(childComplexity int) int
}
ImportFrameworkPayload struct { ImportFrameworkPayload struct {
FrameworkEdge func(childComplexity int) int FrameworkEdge func(childComplexity int) int
} }
@@ -533,6 +537,7 @@ type ComplexityRoot struct {
ExportAudit func(childComplexity int, input types.ExportAuditInput) int ExportAudit func(childComplexity int, input types.ExportAuditInput) int
FulfillEvidence func(childComplexity int, input types.FulfillEvidenceInput) int FulfillEvidence func(childComplexity int, input types.FulfillEvidenceInput) int
GenerateDocumentChangelog func(childComplexity int, input types.GenerateDocumentChangelogInput) 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 ImportFramework func(childComplexity int, input types.ImportFrameworkInput) int
ImportMeasure func(childComplexity int, input types.ImportMeasureInput) int ImportMeasure func(childComplexity int, input types.ImportMeasureInput) int
InviteUser func(childComplexity int, input types.InviteUserInput) int InviteUser func(childComplexity int, input types.InviteUserInput) int
@@ -974,6 +979,7 @@ type MutationResolver interface {
UpdateFramework(ctx context.Context, input types.UpdateFrameworkInput) (*types.UpdateFrameworkPayload, error) UpdateFramework(ctx context.Context, input types.UpdateFrameworkInput) (*types.UpdateFrameworkPayload, error)
ImportFramework(ctx context.Context, input types.ImportFrameworkInput) (*types.ImportFrameworkPayload, error) ImportFramework(ctx context.Context, input types.ImportFrameworkInput) (*types.ImportFrameworkPayload, error)
DeleteFramework(ctx context.Context, input types.DeleteFrameworkInput) (*types.DeleteFrameworkPayload, 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) CreateControl(ctx context.Context, input types.CreateControlInput) (*types.CreateControlPayload, error)
UpdateControl(ctx context.Context, input types.UpdateControlInput) (*types.UpdateControlPayload, error) UpdateControl(ctx context.Context, input types.UpdateControlInput) (*types.UpdateControlPayload, error)
DeleteControl(ctx context.Context, input types.DeleteControlInput) (*types.DeleteControlPayload, 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 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": case "ImportFrameworkPayload.frameworkEdge":
if e.complexity.ImportFrameworkPayload.FrameworkEdge == nil { if e.complexity.ImportFrameworkPayload.FrameworkEdge == nil {
break 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 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": case "Mutation.importFramework":
if e.complexity.Mutation.ImportFramework == nil { if e.complexity.Mutation.ImportFramework == nil {
break break
@@ -4695,6 +4720,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
ec.unmarshalInputFrameworkOrder, ec.unmarshalInputFrameworkOrder,
ec.unmarshalInputFulfillEvidenceInput, ec.unmarshalInputFulfillEvidenceInput,
ec.unmarshalInputGenerateDocumentChangelogInput, ec.unmarshalInputGenerateDocumentChangelogInput,
ec.unmarshalInputGenerateFrameworkStateOfApplicabilityInput,
ec.unmarshalInputImportFrameworkInput, ec.unmarshalInputImportFrameworkInput,
ec.unmarshalInputImportMeasureInput, ec.unmarshalInputImportMeasureInput,
ec.unmarshalInputInviteUserInput, ec.unmarshalInputInviteUserInput,
@@ -6118,6 +6144,9 @@ type Mutation {
updateFramework(input: UpdateFrameworkInput!): UpdateFrameworkPayload! updateFramework(input: UpdateFrameworkInput!): UpdateFrameworkPayload!
importFramework(input: ImportFrameworkInput!): ImportFrameworkPayload! importFramework(input: ImportFrameworkInput!): ImportFrameworkPayload!
deleteFramework(input: DeleteFrameworkInput!): DeleteFrameworkPayload! deleteFramework(input: DeleteFrameworkInput!): DeleteFrameworkPayload!
generateFrameworkStateOfApplicability(
input: GenerateFrameworkStateOfApplicabilityInput!
): GenerateFrameworkStateOfApplicabilityPayload!
# Control mutations # Control mutations
createControl(input: CreateControlInput!): CreateControlPayload! createControl(input: CreateControlInput!): CreateControlPayload!
@@ -6227,6 +6256,14 @@ type Mutation {
} }
# Input Types # Input Types
input GenerateFrameworkStateOfApplicabilityInput {
frameworkId: ID!
}
type GenerateFrameworkStateOfApplicabilityPayload {
downloadUrl: String!
}
input CreateOrganizationInput { input CreateOrganizationInput {
name: String! name: String!
} }
@@ -9273,6 +9310,29 @@ func (ec *executionContext) field_Mutation_generateDocumentChangelog_argsInput(
return zeroVal, nil 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) { func (ec *executionContext) field_Mutation_importFramework_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error var err error
args := map[string]any{} args := map[string]any{}
@@ -20810,6 +20870,50 @@ func (ec *executionContext) fieldContext_GenerateDocumentChangelogPayload_change
return fc, nil 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) { 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) fc, err := ec.fieldContext_ImportFrameworkPayload_frameworkEdge(ctx, field)
if err != nil { if err != nil {
@@ -22659,6 +22763,65 @@ func (ec *executionContext) fieldContext_Mutation_deleteFramework(ctx context.Co
return fc, nil 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) { func (ec *executionContext) _Mutation_createControl(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Mutation_createControl(ctx, field) fc, err := ec.fieldContext_Mutation_createControl(ctx, field)
if err != nil { if err != nil {
@@ -38883,6 +39046,33 @@ func (ec *executionContext) unmarshalInputGenerateDocumentChangelogInput(ctx con
return it, nil 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) { func (ec *executionContext) unmarshalInputImportFrameworkInput(ctx context.Context, obj any) (types.ImportFrameworkInput, error) {
var it types.ImportFrameworkInput var it types.ImportFrameworkInput
asMap := map[string]any{} asMap := map[string]any{}
@@ -44756,6 +44946,45 @@ func (ec *executionContext) _GenerateDocumentChangelogPayload(ctx context.Contex
return out 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"} var importFrameworkPayloadImplementors = []string{"ImportFrameworkPayload"}
func (ec *executionContext) _ImportFrameworkPayload(ctx context.Context, sel ast.SelectionSet, obj *types.ImportFrameworkPayload) graphql.Marshaler { 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 { if out.Values[i] == graphql.Null {
out.Invalids++ 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": case "createControl":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_createControl(ctx, field) return ec._Mutation_createControl(ctx, field)
@@ -51728,6 +51964,25 @@ func (ec *executionContext) marshalNGenerateDocumentChangelogPayload2ᚖgithub
return ec._GenerateDocumentChangelogPayload(ctx, sel, v) 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) { func (ec *executionContext) unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx context.Context, v any) (gid.GID, error) {
res, err := types.UnmarshalGIDScalar(v) res, err := types.UnmarshalGIDScalar(v)
return res, graphql.ErrorOnPath(ctx, err) return res, graphql.ErrorOnPath(ctx, err)

View File

@@ -654,6 +654,14 @@ type GenerateDocumentChangelogPayload struct {
Changelog string `json:"changelog"` Changelog string `json:"changelog"`
} }
type GenerateFrameworkStateOfApplicabilityInput struct {
FrameworkID gid.GID `json:"frameworkId"`
}
type GenerateFrameworkStateOfApplicabilityPayload struct {
DownloadURL string `json:"downloadUrl"`
}
type ImportFrameworkInput struct { type ImportFrameworkInput struct {
OrganizationID gid.GID `json:"organizationId"` OrganizationID gid.GID `json:"organizationId"`
File graphql.Upload `json:"file"` File graphql.Upload `json:"file"`

View File

@@ -1178,6 +1178,20 @@ func (r *mutationResolver) DeleteFramework(ctx context.Context, input types.Dele
}, nil }, 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. // CreateControl is the resolver for the createControl field.
func (r *mutationResolver) CreateControl(ctx context.Context, input types.CreateControlInput) (*types.CreateControlPayload, error) { func (r *mutationResolver) CreateControl(ctx context.Context, input types.CreateControlInput) (*types.CreateControlPayload, error) {
prb := r.ProboService(ctx, input.FrameworkID.TenantID()) prb := r.ProboService(ctx, input.FrameworkID.TenantID())