Refactor SOA generation
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
@@ -26,10 +26,7 @@ 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 {
|
import type { FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation } from "./__generated__/FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation.graphql";
|
||||||
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";
|
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||||
@@ -59,7 +56,7 @@ const generateFrameworkStateOfApplicabilityMutation = graphql`
|
|||||||
generateFrameworkStateOfApplicability(
|
generateFrameworkStateOfApplicability(
|
||||||
input: { frameworkId: $frameworkId }
|
input: { frameworkId: $frameworkId }
|
||||||
) {
|
) {
|
||||||
downloadUrl
|
data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -139,17 +136,14 @@ export default function FrameworkDetailPage(props: Props) {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
generateFrameworkStateOfApplicability({
|
generateFrameworkStateOfApplicability({
|
||||||
variables: { frameworkId: framework.id },
|
variables: { frameworkId: framework.id },
|
||||||
onCompleted: (
|
onCompleted: (data) => {
|
||||||
data: FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation$data
|
if (data.generateFrameworkStateOfApplicability?.data) {
|
||||||
) => {
|
const link = window.document.createElement("a");
|
||||||
if (data.generateFrameworkStateOfApplicability.downloadUrl) {
|
link.href = data.generateFrameworkStateOfApplicability.data;
|
||||||
const link = document.createElement("a");
|
link.download = `${framework.name}-SOA.xlsx`;
|
||||||
link.href =
|
window.document.body.appendChild(link);
|
||||||
data.generateFrameworkStateOfApplicability.downloadUrl;
|
|
||||||
link.download = `${framework.name}-SOA.pdf`; // You can adjust the filename as needed
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
window.document.body.removeChild(link);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<bcdaed0c84805f387ee7d1de8ece0068>>
|
* @generated SignedSource<<b7c54e98eb3ba12bf4619530b45fdbab>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -14,7 +14,7 @@ export type FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation$var
|
|||||||
};
|
};
|
||||||
export type FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation$data = {
|
export type FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation$data = {
|
||||||
readonly generateFrameworkStateOfApplicability: {
|
readonly generateFrameworkStateOfApplicability: {
|
||||||
readonly downloadUrl: string;
|
readonly data: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation = {
|
export type FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation = {
|
||||||
@@ -55,7 +55,7 @@ v1 = [
|
|||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"kind": "ScalarField",
|
"kind": "ScalarField",
|
||||||
"name": "downloadUrl",
|
"name": "data",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -80,16 +80,16 @@ return {
|
|||||||
"selections": (v1/*: any*/)
|
"selections": (v1/*: any*/)
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "338cbffe84fb19cdf46c97b739216342",
|
"cacheID": "80185d474f3a9c5d20f97a387b219511",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation",
|
"name": "FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation",
|
||||||
"operationKind": "mutation",
|
"operationKind": "mutation",
|
||||||
"text": "mutation FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation(\n $frameworkId: ID!\n) {\n generateFrameworkStateOfApplicability(input: {frameworkId: $frameworkId}) {\n downloadUrl\n }\n}\n"
|
"text": "mutation FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation(\n $frameworkId: ID!\n) {\n generateFrameworkStateOfApplicability(input: {frameworkId: $frameworkId}) {\n data\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "f5c0a7d2bebedecc3de7d55d52f06cd8";
|
(node as any).hash = "eac70164d676cb8e19d7fe7f06b2b817";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
|
|||||||
@@ -418,3 +418,45 @@ DELETE FROM risks WHERE %s AND id = @id
|
|||||||
_, err := conn.Exec(ctx, q, args)
|
_, err := conn.Exec(ctx, q, args)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Risks) CountByDocumentID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
documentID gid.GID,
|
||||||
|
filter *RiskFilter,
|
||||||
|
) (int, error) {
|
||||||
|
q := `
|
||||||
|
WITH rsks AS (
|
||||||
|
SELECT
|
||||||
|
r.id,
|
||||||
|
r.tenant_id
|
||||||
|
FROM
|
||||||
|
risks r
|
||||||
|
INNER JOIN
|
||||||
|
risks_documents rd ON r.id = rd.risk_id
|
||||||
|
WHERE
|
||||||
|
rd.document_id = @document_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
COUNT(id)
|
||||||
|
FROM
|
||||||
|
rsks
|
||||||
|
WHERE %s
|
||||||
|
AND %s
|
||||||
|
`
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.NamedArgs{"document_id": documentID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
maps.Copy(args, filter.SQLArguments())
|
||||||
|
|
||||||
|
row := conn.QueryRow(ctx, q, args)
|
||||||
|
|
||||||
|
var count int
|
||||||
|
if err := row.Scan(&count); err != nil {
|
||||||
|
return 0, fmt.Errorf("cannot scan count: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,29 +15,21 @@
|
|||||||
package probo
|
package probo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"archive/tar"
|
|
||||||
"compress/gzip"
|
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go-v2/aws"
|
|
||||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
||||||
"github.com/getprobo/probo/pkg/coredata"
|
"github.com/getprobo/probo/pkg/coredata"
|
||||||
"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"
|
"github.com/getprobo/probo/pkg/soagen"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.gearno.de/x/ref"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
maxStateOfApplicabilityLimit = 10_000
|
||||||
maxControlsLimit = 10000
|
maxControlsLimit = 10000
|
||||||
maxItemsLimit = 1000
|
maxItemsLimit = 1000
|
||||||
presignExpiry = 15 * time.Minute
|
presignExpiry = 15 * time.Minute
|
||||||
@@ -71,18 +63,6 @@ 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(
|
||||||
@@ -292,137 +272,100 @@ func (s FrameworkService) Import(
|
|||||||
return framework, nil
|
return framework, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s FrameworkService) ExportAudit(
|
func (s FrameworkService) StateOfApplicability(ctx context.Context, frameworkID gid.GID) ([]byte, error) {
|
||||||
ctx context.Context,
|
rows := []soagen.SOARowData{}
|
||||||
frameworkID gid.GID,
|
|
||||||
) (string, error) {
|
|
||||||
var archivePath string
|
|
||||||
var objectKey string
|
|
||||||
|
|
||||||
err := s.svc.pg.WithConn(ctx, func(conn pg.Conn) error {
|
err := s.svc.pg.WithTx(
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
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{}
|
controls := coredata.Controls{}
|
||||||
controlsCursor := page.NewCursor(
|
err := controls.LoadByFrameworkID(
|
||||||
0,
|
ctx,
|
||||||
|
conn,
|
||||||
|
s.svc.scope,
|
||||||
|
frameworkID,
|
||||||
|
page.NewCursor(
|
||||||
|
maxStateOfApplicabilityLimit,
|
||||||
nil,
|
nil,
|
||||||
page.Head,
|
page.Head,
|
||||||
page.OrderBy[coredata.ControlOrderField]{
|
page.OrderBy[coredata.ControlOrderField]{
|
||||||
Field: coredata.ControlOrderFieldCreatedAt,
|
Field: coredata.ControlOrderFieldSectionTitle,
|
||||||
Direction: page.OrderDirectionAsc,
|
Direction: page.OrderDirectionAsc,
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
coredata.NewControlFilter(nil),
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := controls.LoadByFrameworkID(ctx, conn, s.svc.scope, frameworkID, controlsCursor, coredata.NewControlFilter(nil)); err != nil {
|
if 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 {
|
row := soagen.SOARowData{
|
||||||
return fmt.Errorf("cannot export control data: %w", err)
|
SectionTitle: control.SectionTitle,
|
||||||
}
|
ControlName: control.Name,
|
||||||
|
Applicability: soagen.NewApplicability("Yes", true),
|
||||||
|
JustificationExclusion: "",
|
||||||
|
Regulatory: ref.Ref(false),
|
||||||
|
Contractual: ref.Ref(false),
|
||||||
|
BestPractice: ref.Ref(true),
|
||||||
|
RiskAssessment: ref.Ref(false),
|
||||||
|
SecurityMeasures: []string{},
|
||||||
}
|
}
|
||||||
|
|
||||||
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))
|
|
||||||
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{}
|
measures := coredata.Measures{}
|
||||||
cursor := page.NewCursor(
|
err = measures.LoadByControlID(
|
||||||
0,
|
ctx,
|
||||||
|
conn,
|
||||||
|
s.svc.scope,
|
||||||
|
control.ID,
|
||||||
|
page.NewCursor(
|
||||||
|
maxStateOfApplicabilityLimit,
|
||||||
nil,
|
nil,
|
||||||
page.Head,
|
page.Head,
|
||||||
page.OrderBy[coredata.MeasureOrderField]{
|
page.OrderBy[coredata.MeasureOrderField]{
|
||||||
Field: coredata.MeasureOrderFieldCreatedAt,
|
Field: coredata.MeasureOrderFieldCreatedAt,
|
||||||
Direction: page.OrderDirectionAsc,
|
Direction: page.OrderDirectionAsc,
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
coredata.NewMeasureFilter(nil),
|
||||||
)
|
)
|
||||||
|
if 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
|
}
|
||||||
|
|
||||||
|
for _, measure := range measures {
|
||||||
|
risks := coredata.Risks{}
|
||||||
|
risksCount, err := risks.CountByMeasureID(
|
||||||
|
ctx,
|
||||||
|
conn,
|
||||||
|
s.svc.scope,
|
||||||
|
measure.ID,
|
||||||
|
coredata.NewRiskFilter(nil),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot count risks: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if risksCount > 0 {
|
||||||
|
row.RiskAssessment = ref.Ref(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
row.SecurityMeasures = append(row.SecurityMeasures, measure.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
err = documents.LoadByControlID(
|
||||||
|
ctx,
|
||||||
|
conn,
|
||||||
|
s.svc.scope,
|
||||||
|
control.ID,
|
||||||
|
page.NewCursor(
|
||||||
0,
|
0,
|
||||||
nil,
|
nil,
|
||||||
page.Head,
|
page.Head,
|
||||||
@@ -430,608 +373,52 @@ func (s FrameworkService) loadDocumentsForControl(ctx context.Context, conn pg.C
|
|||||||
Field: coredata.DocumentOrderFieldCreatedAt,
|
Field: coredata.DocumentOrderFieldCreatedAt,
|
||||||
Direction: page.OrderDirectionAsc,
|
Direction: page.OrderDirectionAsc,
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
coredata.NewDocumentFilter(nil),
|
||||||
)
|
)
|
||||||
|
if 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))
|
|
||||||
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 {
|
|
||||||
return fmt.Errorf("could not create archive file: %w", err)
|
|
||||||
}
|
|
||||||
defer tarFile.Close()
|
|
||||||
|
|
||||||
gzipWriter := io.Writer(tarFile)
|
|
||||||
gzw := gzip.NewWriter(gzipWriter)
|
|
||||||
defer gzw.Close()
|
|
||||||
|
|
||||||
tarWriter := tar.NewWriter(gzw)
|
|
||||||
defer tarWriter.Close()
|
|
||||||
|
|
||||||
err = filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
header, err := tar.FileInfoHeader(info, info.Name())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not create tar header: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
relPath, err := filepath.Rel(sourceDir, path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not get relative path: %w", err)
|
|
||||||
}
|
|
||||||
header.Name = relPath
|
|
||||||
|
|
||||||
if err := tarWriter.WriteHeader(header); err != nil {
|
|
||||||
return fmt.Errorf("could not write tar header: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.Mode().IsRegular() {
|
|
||||||
file, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not open file %s: %w", path, err)
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
if _, err := io.Copy(tarWriter, file); err != nil {
|
|
||||||
return fmt.Errorf("could not copy file content: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
excelReader, err := s.createSOAExcelFile(soaData)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload to S3 and get presigned URL
|
|
||||||
presignedURL, err = s.uploadSOAToS3(ctx, framework, excelReader)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
hasRisks := 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
|
|
||||||
}
|
|
||||||
|
|
||||||
risks := coredata.Risks{}
|
risks := coredata.Risks{}
|
||||||
risksCursor := page.NewCursor(
|
risksCount, err := risks.CountByDocumentID(
|
||||||
maxItemsLimit,
|
ctx,
|
||||||
nil,
|
conn,
|
||||||
page.Head,
|
s.svc.scope,
|
||||||
page.OrderBy[coredata.RiskOrderField]{
|
document.ID,
|
||||||
Field: coredata.RiskOrderFieldCreatedAt,
|
coredata.NewRiskFilter(nil),
|
||||||
Direction: page.OrderDirectionAsc,
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot count risks: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if risksCount > 0 {
|
||||||
|
row.RiskAssessment = ref.Ref(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
row.SecurityMeasures = append(row.SecurityMeasures, document.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows = append(rows, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := risks.LoadByMeasureID(ctx, conn, s.svc.scope, measure.ID, risksCursor, coredata.NewRiskFilter(nil)); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
hasRisks = len(risks) > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
rowData := soaRowData{
|
|
||||||
control: control,
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(measures) > 0 || len(policies) > 0 {
|
|
||||||
rowData.applicability = "Yes"
|
|
||||||
rowData.isApplicable = true
|
|
||||||
rowData.regulatory = "YES"
|
|
||||||
rowData.bestPractice = "YES"
|
|
||||||
|
|
||||||
if hasRisks {
|
|
||||||
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(soaData []soaRowData) (io.Reader, error) {
|
|
||||||
f := excelize.NewFile()
|
|
||||||
if err := f.Close(); err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot close Excel file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sheetName := "State of Applicability"
|
|
||||||
f.SetSheetName("Sheet1", sheetName)
|
|
||||||
|
|
||||||
styles, err := s.createExcelStyles(f)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot create Excel styles: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.setupExcelHeader(f, sheetName, styles); err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot setup Excel header: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.populateExcelData(f, sheetName, soaData, styles); err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot populate Excel data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.applyExcelFormatting(f, sheetName, len(soaData)); err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot apply Excel formatting: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write to buffer
|
|
||||||
buffer, err := f.WriteToBuffer()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot write Excel file to buffer: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to bytes.Reader for seekability (required by S3 SDK)
|
|
||||||
return bytes.NewReader(buffer.Bytes()), 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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
styles["header"] = headerStyle
|
|
||||||
|
|
||||||
cellStyle, err := f.NewStyle(&excelize.Style{
|
output, err := soagen.GenerateExcel(
|
||||||
Border: []excelize.Border{
|
soagen.SOAData{
|
||||||
{Type: "left", Color: "#000000", Style: 1},
|
Rows: rows,
|
||||||
{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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("cannot generate Excel file: %w", 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 {
|
return output, nil
|
||||||
// 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()
|
|
||||||
|
|
||||||
// Sanitize framework name by replacing invalid file name characters
|
|
||||||
sanitizedName := framework.Name
|
|
||||||
invalidChars := []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|", " "}
|
|
||||||
for _, char := range invalidChars {
|
|
||||||
sanitizedName = strings.ReplaceAll(sanitizedName, char, "_")
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("SOA_%s_%s.xlsx",
|
|
||||||
sanitizedName,
|
|
||||||
now.Format("20060102_150405"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s FrameworkService) uploadSOAToS3(ctx context.Context, framework *coredata.Framework, excelReader io.Reader) (string, error) {
|
|
||||||
objectKey := fmt.Sprintf("soa/%s/%s", framework.ID, s.generateSOAFileName(framework))
|
|
||||||
|
|
||||||
// Upload file to S3 using bytes reader
|
|
||||||
_, err := s.svc.s3.PutObject(ctx, &s3.PutObjectInput{
|
|
||||||
Bucket: aws.String(s.svc.bucket),
|
|
||||||
Key: aws.String(objectKey),
|
|
||||||
Body: excelReader,
|
|
||||||
})
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1414,8 +1414,6 @@ type Mutation {
|
|||||||
input: CreateVendorRiskAssessmentInput!
|
input: CreateVendorRiskAssessmentInput!
|
||||||
): CreateVendorRiskAssessmentPayload!
|
): CreateVendorRiskAssessmentPayload!
|
||||||
|
|
||||||
exportAudit(input: ExportAuditInput!): ExportAuditPayload!
|
|
||||||
|
|
||||||
assessVendor(input: AssessVendorInput!): AssessVendorPayload!
|
assessVendor(input: AssessVendorInput!): AssessVendorPayload!
|
||||||
|
|
||||||
createAsset(input: CreateAssetInput!): CreateAssetPayload!
|
createAsset(input: CreateAssetInput!): CreateAssetPayload!
|
||||||
@@ -1433,7 +1431,7 @@ input GenerateFrameworkStateOfApplicabilityInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GenerateFrameworkStateOfApplicabilityPayload {
|
type GenerateFrameworkStateOfApplicabilityPayload {
|
||||||
downloadUrl: String!
|
data: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
input CreateOrganizationInput {
|
input CreateOrganizationInput {
|
||||||
@@ -2185,14 +2183,6 @@ input UploadMeasureEvidenceInput {
|
|||||||
file: Upload!
|
file: Upload!
|
||||||
}
|
}
|
||||||
|
|
||||||
input ExportAuditInput {
|
|
||||||
frameworkId: ID!
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExportAuditPayload {
|
|
||||||
url: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
input GenerateDocumentChangelogInput {
|
input GenerateDocumentChangelogInput {
|
||||||
documentId: ID!
|
documentId: ID!
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -436,10 +436,6 @@ type ComplexityRoot struct {
|
|||||||
Node func(childComplexity int) int
|
Node func(childComplexity int) int
|
||||||
}
|
}
|
||||||
|
|
||||||
ExportAuditPayload struct {
|
|
||||||
URL func(childComplexity int) int
|
|
||||||
}
|
|
||||||
|
|
||||||
ExportDocumentVersionPDFPayload struct {
|
ExportDocumentVersionPDFPayload struct {
|
||||||
Data func(childComplexity int) int
|
Data func(childComplexity int) int
|
||||||
}
|
}
|
||||||
@@ -474,7 +470,7 @@ type ComplexityRoot struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
GenerateFrameworkStateOfApplicabilityPayload struct {
|
GenerateFrameworkStateOfApplicabilityPayload struct {
|
||||||
DownloadURL func(childComplexity int) int
|
Data func(childComplexity int) int
|
||||||
}
|
}
|
||||||
|
|
||||||
ImportFrameworkPayload struct {
|
ImportFrameworkPayload struct {
|
||||||
@@ -554,7 +550,6 @@ type ComplexityRoot struct {
|
|||||||
DeleteTask func(childComplexity int, input types.DeleteTaskInput) int
|
DeleteTask func(childComplexity int, input types.DeleteTaskInput) int
|
||||||
DeleteVendor func(childComplexity int, input types.DeleteVendorInput) int
|
DeleteVendor func(childComplexity int, input types.DeleteVendorInput) int
|
||||||
DeleteVendorComplianceReport func(childComplexity int, input types.DeleteVendorComplianceReportInput) int
|
DeleteVendorComplianceReport func(childComplexity int, input types.DeleteVendorComplianceReportInput) int
|
||||||
ExportAudit func(childComplexity int, input types.ExportAuditInput) int
|
|
||||||
ExportDocumentVersionPDF func(childComplexity int, input types.ExportDocumentVersionPDFInput) int
|
ExportDocumentVersionPDF func(childComplexity int, input types.ExportDocumentVersionPDFInput) 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
|
||||||
@@ -1045,7 +1040,6 @@ type MutationResolver interface {
|
|||||||
CancelSignatureRequest(ctx context.Context, input types.CancelSignatureRequestInput) (*types.CancelSignatureRequestPayload, error)
|
CancelSignatureRequest(ctx context.Context, input types.CancelSignatureRequestInput) (*types.CancelSignatureRequestPayload, error)
|
||||||
ExportDocumentVersionPDF(ctx context.Context, input types.ExportDocumentVersionPDFInput) (*types.ExportDocumentVersionPDFPayload, error)
|
ExportDocumentVersionPDF(ctx context.Context, input types.ExportDocumentVersionPDFInput) (*types.ExportDocumentVersionPDFPayload, error)
|
||||||
CreateVendorRiskAssessment(ctx context.Context, input types.CreateVendorRiskAssessmentInput) (*types.CreateVendorRiskAssessmentPayload, error)
|
CreateVendorRiskAssessment(ctx context.Context, input types.CreateVendorRiskAssessmentInput) (*types.CreateVendorRiskAssessmentPayload, error)
|
||||||
ExportAudit(ctx context.Context, input types.ExportAuditInput) (*types.ExportAuditPayload, error)
|
|
||||||
AssessVendor(ctx context.Context, input types.AssessVendorInput) (*types.AssessVendorPayload, error)
|
AssessVendor(ctx context.Context, input types.AssessVendorInput) (*types.AssessVendorPayload, error)
|
||||||
CreateAsset(ctx context.Context, input types.CreateAssetInput) (*types.CreateAssetPayload, error)
|
CreateAsset(ctx context.Context, input types.CreateAssetInput) (*types.CreateAssetPayload, error)
|
||||||
UpdateAsset(ctx context.Context, input types.UpdateAssetInput) (*types.UpdateAssetPayload, error)
|
UpdateAsset(ctx context.Context, input types.UpdateAssetInput) (*types.UpdateAssetPayload, error)
|
||||||
@@ -2338,13 +2332,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
|||||||
|
|
||||||
return e.complexity.EvidenceEdge.Node(childComplexity), true
|
return e.complexity.EvidenceEdge.Node(childComplexity), true
|
||||||
|
|
||||||
case "ExportAuditPayload.url":
|
|
||||||
if e.complexity.ExportAuditPayload.URL == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.complexity.ExportAuditPayload.URL(childComplexity), true
|
|
||||||
|
|
||||||
case "ExportDocumentVersionPDFPayload.data":
|
case "ExportDocumentVersionPDFPayload.data":
|
||||||
if e.complexity.ExportDocumentVersionPDFPayload.Data == nil {
|
if e.complexity.ExportDocumentVersionPDFPayload.Data == nil {
|
||||||
break
|
break
|
||||||
@@ -2455,12 +2442,12 @@ 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":
|
case "GenerateFrameworkStateOfApplicabilityPayload.data":
|
||||||
if e.complexity.GenerateFrameworkStateOfApplicabilityPayload.DownloadURL == nil {
|
if e.complexity.GenerateFrameworkStateOfApplicabilityPayload.Data == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
return e.complexity.GenerateFrameworkStateOfApplicabilityPayload.DownloadURL(childComplexity), true
|
return e.complexity.GenerateFrameworkStateOfApplicabilityPayload.Data(childComplexity), true
|
||||||
|
|
||||||
case "ImportFrameworkPayload.frameworkEdge":
|
case "ImportFrameworkPayload.frameworkEdge":
|
||||||
if e.complexity.ImportFrameworkPayload.FrameworkEdge == nil {
|
if e.complexity.ImportFrameworkPayload.FrameworkEdge == nil {
|
||||||
@@ -3083,18 +3070,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
|||||||
|
|
||||||
return e.complexity.Mutation.DeleteVendorComplianceReport(childComplexity, args["input"].(types.DeleteVendorComplianceReportInput)), true
|
return e.complexity.Mutation.DeleteVendorComplianceReport(childComplexity, args["input"].(types.DeleteVendorComplianceReportInput)), true
|
||||||
|
|
||||||
case "Mutation.exportAudit":
|
|
||||||
if e.complexity.Mutation.ExportAudit == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
args, err := ec.field_Mutation_exportAudit_args(ctx, rawArgs)
|
|
||||||
if err != nil {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.complexity.Mutation.ExportAudit(childComplexity, args["input"].(types.ExportAuditInput)), true
|
|
||||||
|
|
||||||
case "Mutation.exportDocumentVersionPDF":
|
case "Mutation.exportDocumentVersionPDF":
|
||||||
if e.complexity.Mutation.ExportDocumentVersionPDF == nil {
|
if e.complexity.Mutation.ExportDocumentVersionPDF == nil {
|
||||||
break
|
break
|
||||||
@@ -4827,7 +4802,6 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
|
|||||||
ec.unmarshalInputDocumentVersionOrder,
|
ec.unmarshalInputDocumentVersionOrder,
|
||||||
ec.unmarshalInputDocumentVersionSignatureOrder,
|
ec.unmarshalInputDocumentVersionSignatureOrder,
|
||||||
ec.unmarshalInputEvidenceOrder,
|
ec.unmarshalInputEvidenceOrder,
|
||||||
ec.unmarshalInputExportAuditInput,
|
|
||||||
ec.unmarshalInputExportDocumentVersionPDFInput,
|
ec.unmarshalInputExportDocumentVersionPDFInput,
|
||||||
ec.unmarshalInputFrameworkOrder,
|
ec.unmarshalInputFrameworkOrder,
|
||||||
ec.unmarshalInputFulfillEvidenceInput,
|
ec.unmarshalInputFulfillEvidenceInput,
|
||||||
@@ -6381,8 +6355,6 @@ type Mutation {
|
|||||||
input: CreateVendorRiskAssessmentInput!
|
input: CreateVendorRiskAssessmentInput!
|
||||||
): CreateVendorRiskAssessmentPayload!
|
): CreateVendorRiskAssessmentPayload!
|
||||||
|
|
||||||
exportAudit(input: ExportAuditInput!): ExportAuditPayload!
|
|
||||||
|
|
||||||
assessVendor(input: AssessVendorInput!): AssessVendorPayload!
|
assessVendor(input: AssessVendorInput!): AssessVendorPayload!
|
||||||
|
|
||||||
createAsset(input: CreateAssetInput!): CreateAssetPayload!
|
createAsset(input: CreateAssetInput!): CreateAssetPayload!
|
||||||
@@ -6400,7 +6372,7 @@ input GenerateFrameworkStateOfApplicabilityInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GenerateFrameworkStateOfApplicabilityPayload {
|
type GenerateFrameworkStateOfApplicabilityPayload {
|
||||||
downloadUrl: String!
|
data: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
input CreateOrganizationInput {
|
input CreateOrganizationInput {
|
||||||
@@ -7152,14 +7124,6 @@ input UploadMeasureEvidenceInput {
|
|||||||
file: Upload!
|
file: Upload!
|
||||||
}
|
}
|
||||||
|
|
||||||
input ExportAuditInput {
|
|
||||||
frameworkId: ID!
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExportAuditPayload {
|
|
||||||
url: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
input GenerateDocumentChangelogInput {
|
input GenerateDocumentChangelogInput {
|
||||||
documentId: ID!
|
documentId: ID!
|
||||||
}
|
}
|
||||||
@@ -9484,29 +9448,6 @@ func (ec *executionContext) field_Mutation_deleteVendor_argsInput(
|
|||||||
return zeroVal, nil
|
return zeroVal, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) field_Mutation_exportAudit_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
|
|
||||||
var err error
|
|
||||||
args := map[string]any{}
|
|
||||||
arg0, err := ec.field_Mutation_exportAudit_argsInput(ctx, rawArgs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
args["input"] = arg0
|
|
||||||
return args, nil
|
|
||||||
}
|
|
||||||
func (ec *executionContext) field_Mutation_exportAudit_argsInput(
|
|
||||||
ctx context.Context,
|
|
||||||
rawArgs map[string]any,
|
|
||||||
) (types.ExportAuditInput, error) {
|
|
||||||
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input"))
|
|
||||||
if tmp, ok := rawArgs["input"]; ok {
|
|
||||||
return ec.unmarshalNExportAuditInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐExportAuditInput(ctx, tmp)
|
|
||||||
}
|
|
||||||
|
|
||||||
var zeroVal types.ExportAuditInput
|
|
||||||
return zeroVal, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) field_Mutation_exportDocumentVersionPDF_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
|
func (ec *executionContext) field_Mutation_exportDocumentVersionPDF_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{}
|
||||||
@@ -20577,50 +20518,6 @@ func (ec *executionContext) fieldContext_EvidenceEdge_node(_ context.Context, fi
|
|||||||
return fc, nil
|
return fc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _ExportAuditPayload_url(ctx context.Context, field graphql.CollectedField, obj *types.ExportAuditPayload) (ret graphql.Marshaler) {
|
|
||||||
fc, err := ec.fieldContext_ExportAuditPayload_url(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.URL, 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_ExportAuditPayload_url(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
|
||||||
fc = &graphql.FieldContext{
|
|
||||||
Object: "ExportAuditPayload",
|
|
||||||
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) _ExportDocumentVersionPDFPayload_data(ctx context.Context, field graphql.CollectedField, obj *types.ExportDocumentVersionPDFPayload) (ret graphql.Marshaler) {
|
func (ec *executionContext) _ExportDocumentVersionPDFPayload_data(ctx context.Context, field graphql.CollectedField, obj *types.ExportDocumentVersionPDFPayload) (ret graphql.Marshaler) {
|
||||||
fc, err := ec.fieldContext_ExportDocumentVersionPDFPayload_data(ctx, field)
|
fc, err := ec.fieldContext_ExportDocumentVersionPDFPayload_data(ctx, field)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -21374,8 +21271,8 @@ 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) {
|
func (ec *executionContext) _GenerateFrameworkStateOfApplicabilityPayload_data(ctx context.Context, field graphql.CollectedField, obj *types.GenerateFrameworkStateOfApplicabilityPayload) (ret graphql.Marshaler) {
|
||||||
fc, err := ec.fieldContext_GenerateFrameworkStateOfApplicabilityPayload_downloadUrl(ctx, field)
|
fc, err := ec.fieldContext_GenerateFrameworkStateOfApplicabilityPayload_data(ctx, field)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
@@ -21388,7 +21285,7 @@ func (ec *executionContext) _GenerateFrameworkStateOfApplicabilityPayload_downlo
|
|||||||
}()
|
}()
|
||||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
||||||
ctx = rctx // use context from middleware stack in children
|
ctx = rctx // use context from middleware stack in children
|
||||||
return obj.DownloadURL, nil
|
return obj.Data, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ec.Error(ctx, err)
|
ec.Error(ctx, err)
|
||||||
@@ -21405,7 +21302,7 @@ func (ec *executionContext) _GenerateFrameworkStateOfApplicabilityPayload_downlo
|
|||||||
return ec.marshalNString2string(ctx, field.Selections, res)
|
return ec.marshalNString2string(ctx, field.Selections, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) fieldContext_GenerateFrameworkStateOfApplicabilityPayload_downloadUrl(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
func (ec *executionContext) fieldContext_GenerateFrameworkStateOfApplicabilityPayload_data(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
fc = &graphql.FieldContext{
|
fc = &graphql.FieldContext{
|
||||||
Object: "GenerateFrameworkStateOfApplicabilityPayload",
|
Object: "GenerateFrameworkStateOfApplicabilityPayload",
|
||||||
Field: field,
|
Field: field,
|
||||||
@@ -23306,8 +23203,8 @@ func (ec *executionContext) fieldContext_Mutation_generateFrameworkStateOfApplic
|
|||||||
IsResolver: true,
|
IsResolver: true,
|
||||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
switch field.Name {
|
switch field.Name {
|
||||||
case "downloadUrl":
|
case "data":
|
||||||
return ec.fieldContext_GenerateFrameworkStateOfApplicabilityPayload_downloadUrl(ctx, field)
|
return ec.fieldContext_GenerateFrameworkStateOfApplicabilityPayload_data(ctx, field)
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("no field named %q was found under type GenerateFrameworkStateOfApplicabilityPayload", field.Name)
|
return nil, fmt.Errorf("no field named %q was found under type GenerateFrameworkStateOfApplicabilityPayload", field.Name)
|
||||||
},
|
},
|
||||||
@@ -25944,65 +25841,6 @@ func (ec *executionContext) fieldContext_Mutation_createVendorRiskAssessment(ctx
|
|||||||
return fc, nil
|
return fc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _Mutation_exportAudit(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
|
||||||
fc, err := ec.fieldContext_Mutation_exportAudit(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().ExportAudit(rctx, fc.Args["input"].(types.ExportAuditInput))
|
|
||||||
})
|
|
||||||
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.ExportAuditPayload)
|
|
||||||
fc.Result = res
|
|
||||||
return ec.marshalNExportAuditPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐExportAuditPayload(ctx, field.Selections, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) fieldContext_Mutation_exportAudit(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 "url":
|
|
||||||
return ec.fieldContext_ExportAuditPayload_url(ctx, field)
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("no field named %q was found under type ExportAuditPayload", 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_exportAudit_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
|
|
||||||
ec.Error(ctx, err)
|
|
||||||
return fc, err
|
|
||||||
}
|
|
||||||
return fc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) _Mutation_assessVendor(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
func (ec *executionContext) _Mutation_assessVendor(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
fc, err := ec.fieldContext_Mutation_assessVendor(ctx, field)
|
fc, err := ec.fieldContext_Mutation_assessVendor(ctx, field)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -39747,33 +39585,6 @@ func (ec *executionContext) unmarshalInputEvidenceOrder(ctx context.Context, obj
|
|||||||
return it, nil
|
return it, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) unmarshalInputExportAuditInput(ctx context.Context, obj any) (types.ExportAuditInput, error) {
|
|
||||||
var it types.ExportAuditInput
|
|
||||||
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) unmarshalInputExportDocumentVersionPDFInput(ctx context.Context, obj any) (types.ExportDocumentVersionPDFInput, error) {
|
func (ec *executionContext) unmarshalInputExportDocumentVersionPDFInput(ctx context.Context, obj any) (types.ExportDocumentVersionPDFInput, error) {
|
||||||
var it types.ExportDocumentVersionPDFInput
|
var it types.ExportDocumentVersionPDFInput
|
||||||
asMap := map[string]any{}
|
asMap := map[string]any{}
|
||||||
@@ -45560,45 +45371,6 @@ func (ec *executionContext) _EvidenceEdge(ctx context.Context, sel ast.Selection
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
var exportAuditPayloadImplementors = []string{"ExportAuditPayload"}
|
|
||||||
|
|
||||||
func (ec *executionContext) _ExportAuditPayload(ctx context.Context, sel ast.SelectionSet, obj *types.ExportAuditPayload) graphql.Marshaler {
|
|
||||||
fields := graphql.CollectFields(ec.OperationContext, sel, exportAuditPayloadImplementors)
|
|
||||||
|
|
||||||
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("ExportAuditPayload")
|
|
||||||
case "url":
|
|
||||||
out.Values[i] = ec._ExportAuditPayload_url(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 exportDocumentVersionPDFPayloadImplementors = []string{"ExportDocumentVersionPDFPayload"}
|
var exportDocumentVersionPDFPayloadImplementors = []string{"ExportDocumentVersionPDFPayload"}
|
||||||
|
|
||||||
func (ec *executionContext) _ExportDocumentVersionPDFPayload(ctx context.Context, sel ast.SelectionSet, obj *types.ExportDocumentVersionPDFPayload) graphql.Marshaler {
|
func (ec *executionContext) _ExportDocumentVersionPDFPayload(ctx context.Context, sel ast.SelectionSet, obj *types.ExportDocumentVersionPDFPayload) graphql.Marshaler {
|
||||||
@@ -45982,8 +45754,8 @@ func (ec *executionContext) _GenerateFrameworkStateOfApplicabilityPayload(ctx co
|
|||||||
switch field.Name {
|
switch field.Name {
|
||||||
case "__typename":
|
case "__typename":
|
||||||
out.Values[i] = graphql.MarshalString("GenerateFrameworkStateOfApplicabilityPayload")
|
out.Values[i] = graphql.MarshalString("GenerateFrameworkStateOfApplicabilityPayload")
|
||||||
case "downloadUrl":
|
case "data":
|
||||||
out.Values[i] = ec._GenerateFrameworkStateOfApplicabilityPayload_downloadUrl(ctx, field, obj)
|
out.Values[i] = ec._GenerateFrameworkStateOfApplicabilityPayload_data(ctx, field, obj)
|
||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
out.Invalids++
|
out.Invalids++
|
||||||
}
|
}
|
||||||
@@ -46903,13 +46675,6 @@ 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 "exportAudit":
|
|
||||||
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
|
|
||||||
return ec._Mutation_exportAudit(ctx, field)
|
|
||||||
})
|
|
||||||
if out.Values[i] == graphql.Null {
|
|
||||||
out.Invalids++
|
|
||||||
}
|
|
||||||
case "assessVendor":
|
case "assessVendor":
|
||||||
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_assessVendor(ctx, field)
|
return ec._Mutation_assessVendor(ctx, field)
|
||||||
@@ -52911,25 +52676,6 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ec *executionContext) unmarshalNExportAuditInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐExportAuditInput(ctx context.Context, v any) (types.ExportAuditInput, error) {
|
|
||||||
res, err := ec.unmarshalInputExportAuditInput(ctx, v)
|
|
||||||
return res, graphql.ErrorOnPath(ctx, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) marshalNExportAuditPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐExportAuditPayload(ctx context.Context, sel ast.SelectionSet, v types.ExportAuditPayload) graphql.Marshaler {
|
|
||||||
return ec._ExportAuditPayload(ctx, sel, &v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) marshalNExportAuditPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐExportAuditPayload(ctx context.Context, sel ast.SelectionSet, v *types.ExportAuditPayload) 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._ExportAuditPayload(ctx, sel, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) unmarshalNExportDocumentVersionPDFInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐExportDocumentVersionPDFInput(ctx context.Context, v any) (types.ExportDocumentVersionPDFInput, error) {
|
func (ec *executionContext) unmarshalNExportDocumentVersionPDFInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐExportDocumentVersionPDFInput(ctx context.Context, v any) (types.ExportDocumentVersionPDFInput, error) {
|
||||||
res, err := ec.unmarshalInputExportDocumentVersionPDFInput(ctx, v)
|
res, err := ec.unmarshalInputExportDocumentVersionPDFInput(ctx, v)
|
||||||
return res, graphql.ErrorOnPath(ctx, err)
|
return res, graphql.ErrorOnPath(ctx, err)
|
||||||
|
|||||||
@@ -636,14 +636,6 @@ type EvidenceEdge struct {
|
|||||||
Node *Evidence `json:"node"`
|
Node *Evidence `json:"node"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExportAuditInput struct {
|
|
||||||
FrameworkID gid.GID `json:"frameworkId"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExportAuditPayload struct {
|
|
||||||
URL string `json:"url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExportDocumentVersionPDFInput struct {
|
type ExportDocumentVersionPDFInput struct {
|
||||||
DocumentVersionID gid.GID `json:"documentVersionId"`
|
DocumentVersionID gid.GID `json:"documentVersionId"`
|
||||||
}
|
}
|
||||||
@@ -694,7 +686,7 @@ type GenerateFrameworkStateOfApplicabilityInput struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GenerateFrameworkStateOfApplicabilityPayload struct {
|
type GenerateFrameworkStateOfApplicabilityPayload struct {
|
||||||
DownloadURL string `json:"downloadUrl"`
|
Data string `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImportFrameworkInput struct {
|
type ImportFrameworkInput struct {
|
||||||
|
|||||||
@@ -1189,7 +1189,10 @@ func (r *mutationResolver) GenerateFrameworkStateOfApplicability(ctx context.Con
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &types.GenerateFrameworkStateOfApplicabilityPayload{
|
return &types.GenerateFrameworkStateOfApplicabilityPayload{
|
||||||
DownloadURL: soa,
|
Data: fmt.Sprintf(
|
||||||
|
"data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,%s",
|
||||||
|
base64.StdEncoding.EncodeToString(soa),
|
||||||
|
),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2051,19 +2054,6 @@ func (r *mutationResolver) CreateVendorRiskAssessment(ctx context.Context, input
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExportAudit is the resolver for the exportAudit field.
|
|
||||||
func (r *mutationResolver) ExportAudit(ctx context.Context, input types.ExportAuditInput) (*types.ExportAuditPayload, error) {
|
|
||||||
prb := r.ProboService(ctx, input.FrameworkID.TenantID())
|
|
||||||
fileUrl, err := prb.Frameworks.ExportAudit(ctx, input.FrameworkID)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("cannot export audit: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return &types.ExportAuditPayload{
|
|
||||||
URL: fileUrl,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssessVendor is the resolver for the assessVendor field.
|
// AssessVendor is the resolver for the assessVendor field.
|
||||||
func (r *mutationResolver) AssessVendor(ctx context.Context, input types.AssessVendorInput) (*types.AssessVendorPayload, error) {
|
func (r *mutationResolver) AssessVendor(ctx context.Context, input types.AssessVendorInput) (*types.AssessVendorPayload, error) {
|
||||||
prb := r.ProboService(ctx, input.ID.TenantID())
|
prb := r.ProboService(ctx, input.ID.TenantID())
|
||||||
|
|||||||
56
pkg/soagen/applicability.go
Normal file
56
pkg/soagen/applicability.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
package soagen
|
||||||
|
|
||||||
|
import "github.com/xuri/excelize/v2"
|
||||||
|
|
||||||
|
type Applicability struct {
|
||||||
|
Value string
|
||||||
|
IsApplicable bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApplicability(value string, isApplicable bool) Applicability {
|
||||||
|
return Applicability{
|
||||||
|
Value: value,
|
||||||
|
IsApplicable: isApplicable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Applicability) String() string {
|
||||||
|
return a.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Applicability) MarshalExcel() ExcelValue {
|
||||||
|
color := "#FFB6C1" // Light red for not applicable
|
||||||
|
if a.IsApplicable {
|
||||||
|
color = "#90EE90" // Light green for applicable
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExcelValue{
|
||||||
|
Value: a.Value,
|
||||||
|
Style: &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{color}, Pattern: 1},
|
||||||
|
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center", WrapText: true},
|
||||||
|
},
|
||||||
|
Validation: []string{"Yes", "No"},
|
||||||
|
Width: 12,
|
||||||
|
}
|
||||||
|
}
|
||||||
28
pkg/soagen/excel_value.go
Normal file
28
pkg/soagen/excel_value.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
package soagen
|
||||||
|
|
||||||
|
import "github.com/xuri/excelize/v2"
|
||||||
|
|
||||||
|
type ExcelValue struct {
|
||||||
|
Value interface{}
|
||||||
|
Style *excelize.Style
|
||||||
|
Validation []string
|
||||||
|
Width float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExcelMarshaler interface {
|
||||||
|
MarshalExcel() ExcelValue
|
||||||
|
}
|
||||||
101
pkg/soagen/field_config.go
Normal file
101
pkg/soagen/field_config.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
package soagen
|
||||||
|
|
||||||
|
type FieldConfiguration struct {
|
||||||
|
Field string
|
||||||
|
Columns []string
|
||||||
|
FilterColumns []string // Specific columns that should have filters (subset of Columns)
|
||||||
|
Width []float64 // Width for each column
|
||||||
|
DefaultWidth float64 // Default width if not specified
|
||||||
|
HasFilter bool // Whether to enable auto-filter
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
soaFieldConfigs = []FieldConfiguration{
|
||||||
|
{
|
||||||
|
Field: "SectionTitle",
|
||||||
|
Columns: []string{"A"},
|
||||||
|
FilterColumns: []string{},
|
||||||
|
Width: []float64{12},
|
||||||
|
DefaultWidth: 12,
|
||||||
|
HasFilter: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Field: "ControlName",
|
||||||
|
Columns: []string{"B"},
|
||||||
|
FilterColumns: []string{},
|
||||||
|
Width: []float64{35},
|
||||||
|
DefaultWidth: 35,
|
||||||
|
HasFilter: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Field: "Applicability",
|
||||||
|
Columns: []string{"C"},
|
||||||
|
FilterColumns: []string{"C"},
|
||||||
|
Width: []float64{12},
|
||||||
|
DefaultWidth: 12,
|
||||||
|
HasFilter: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Field: "JustificationExclusion",
|
||||||
|
Columns: []string{"D"},
|
||||||
|
FilterColumns: []string{},
|
||||||
|
Width: []float64{25},
|
||||||
|
DefaultWidth: 25,
|
||||||
|
HasFilter: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Field: "Regulatory",
|
||||||
|
Columns: []string{"E"},
|
||||||
|
FilterColumns: []string{},
|
||||||
|
Width: []float64{12},
|
||||||
|
DefaultWidth: 12,
|
||||||
|
HasFilter: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Field: "Contractual",
|
||||||
|
Columns: []string{"F"},
|
||||||
|
FilterColumns: []string{},
|
||||||
|
Width: []float64{12},
|
||||||
|
DefaultWidth: 12,
|
||||||
|
HasFilter: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Field: "BestPractice",
|
||||||
|
Columns: []string{"G"},
|
||||||
|
FilterColumns: []string{},
|
||||||
|
Width: []float64{12},
|
||||||
|
DefaultWidth: 12,
|
||||||
|
HasFilter: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Field: "RiskAssessment",
|
||||||
|
Columns: []string{"H"},
|
||||||
|
FilterColumns: []string{},
|
||||||
|
Width: []float64{12},
|
||||||
|
DefaultWidth: 12,
|
||||||
|
HasFilter: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Field: "SecurityMeasures",
|
||||||
|
Columns: []string{"I"},
|
||||||
|
FilterColumns: []string{},
|
||||||
|
Width: []float64{40},
|
||||||
|
DefaultWidth: 40,
|
||||||
|
HasFilter: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
301
pkg/soagen/generator.go
Normal file
301
pkg/soagen/generator.go
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
package soagen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/xuri/excelize/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateSOAExcel(data SOAData) ([]byte, error) {
|
||||||
|
f := excelize.NewFile()
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
sheetName := "State of Applicability"
|
||||||
|
f.NewSheet(sheetName)
|
||||||
|
f.DeleteSheet("Sheet1")
|
||||||
|
|
||||||
|
if err := setupSOAHeader(f, sheetName); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot setup Excel header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := populateSOAData(f, sheetName, data.Rows); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot populate Excel data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := applySOAFinalFormatting(f, sheetName, len(data.Rows)); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot apply final formatting: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := f.WriteToBuffer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot write Excel file to buffer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupSOAHeader(f *excelize.File, sheetName string) error {
|
||||||
|
headerStyle, err := f.NewStyle(getHeaderStyle())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cellStyle, err := f.NewStyle(getCellStyle())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return applyHeaderLayout(f, sheetName, getSOAHeaderLayout(), headerStyle, cellStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func populateSOAData(f *excelize.File, sheetName string, rows []SOARowData) error {
|
||||||
|
dataStartRow := 8
|
||||||
|
|
||||||
|
for i, rowData := range rows {
|
||||||
|
isFirstRow := i == 0
|
||||||
|
filterColumns, err := writeSOARow(f, sheetName, dataStartRow+i, rowData, isFirstRow)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot write row %d: %w", dataStartRow+i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isFirstRow && len(filterColumns) > 0 {
|
||||||
|
if err := applySOAAutoFilter(f, sheetName, filterColumns); err != nil {
|
||||||
|
return fmt.Errorf("cannot apply auto filter: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeSOARow(f *excelize.File, sheetName string, row int, data SOARowData, isFirstRow bool) ([]string, error) {
|
||||||
|
var allFilterColumns []string
|
||||||
|
|
||||||
|
fields := []struct {
|
||||||
|
name string
|
||||||
|
value interface{}
|
||||||
|
}{
|
||||||
|
{"SectionTitle", data.SectionTitle},
|
||||||
|
{"ControlName", data.ControlName},
|
||||||
|
{"Applicability", data.Applicability},
|
||||||
|
{"Regulatory", data.Regulatory},
|
||||||
|
{"Contractual", data.Contractual},
|
||||||
|
{"BestPractice", data.BestPractice},
|
||||||
|
{"RiskAssessment", data.RiskAssessment},
|
||||||
|
{"JustificationExclusion", data.JustificationExclusion},
|
||||||
|
{"SecurityMeasures", data.SecurityMeasures},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, field := range fields {
|
||||||
|
fieldConfig := getSOAFieldConfig(field.name)
|
||||||
|
if fieldConfig == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
filterColumns, err := processField(f, sheetName, row, field.value, *fieldConfig, isFirstRow)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot write field %s: %w", field.name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
allFilterColumns = append(allFilterColumns, filterColumns...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return allFilterColumns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSOAFieldConfig(fieldName string) *FieldConfiguration {
|
||||||
|
for _, config := range soaFieldConfigs {
|
||||||
|
if config.Field == fieldName {
|
||||||
|
return &config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func applySOAAutoFilter(f *excelize.File, sheetName string, filterColumns []string) error {
|
||||||
|
if len(filterColumns) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
firstCol, lastCol := filterColumns[0], filterColumns[0]
|
||||||
|
for _, col := range filterColumns {
|
||||||
|
if col < firstCol {
|
||||||
|
firstCol = col
|
||||||
|
}
|
||||||
|
if col > lastCol {
|
||||||
|
lastCol = col
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filterRange := fmt.Sprintf("%s7:%s1000", firstCol, lastCol)
|
||||||
|
return f.AutoFilter(sheetName, filterRange, []excelize.AutoFilterOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func applySOAFinalFormatting(f *excelize.File, sheetName string, dataRowCount int) error {
|
||||||
|
if dataRowCount > 0 {
|
||||||
|
headerRowHeight := 30.0
|
||||||
|
f.SetRowHeight(sheetName, 6, headerRowHeight)
|
||||||
|
f.SetRowHeight(sheetName, 7, headerRowHeight)
|
||||||
|
|
||||||
|
return f.SetPanes(sheetName, &excelize.Panes{
|
||||||
|
Freeze: true,
|
||||||
|
XSplit: 0,
|
||||||
|
YSplit: 7,
|
||||||
|
TopLeftCell: "A8",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func processField(f *excelize.File, sheetName string, row int, value interface{}, config FieldConfiguration, isFirstRow bool) ([]string, error) {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case ExcelMarshaler:
|
||||||
|
marshaler := v
|
||||||
|
excelValue := marshaler.MarshalExcel()
|
||||||
|
|
||||||
|
styleID, err := createCellStyle(f, excelValue.Style)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return writeSingleColumnField(f, sheetName, row, excelValue, &config, styleID, isFirstRow)
|
||||||
|
|
||||||
|
case string:
|
||||||
|
if len(config.Columns) == 0 {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
col := config.Columns[0]
|
||||||
|
cellRef := fmt.Sprintf("%s%d", col, row)
|
||||||
|
|
||||||
|
f.SetCellValue(sheetName, cellRef, v)
|
||||||
|
|
||||||
|
textStyleID, err := createCellStyle(f, getTextStyle())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot create text style: %w", err)
|
||||||
|
}
|
||||||
|
f.SetCellStyle(sheetName, cellRef, cellRef, textStyleID)
|
||||||
|
|
||||||
|
if isFirstRow {
|
||||||
|
width := config.DefaultWidth
|
||||||
|
if len(config.Width) > 0 {
|
||||||
|
width = config.Width[0]
|
||||||
|
}
|
||||||
|
f.SetColWidth(sheetName, col, col, width)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.HasFilter {
|
||||||
|
if len(config.FilterColumns) > 0 {
|
||||||
|
return config.FilterColumns, nil
|
||||||
|
}
|
||||||
|
return []string{col}, nil
|
||||||
|
}
|
||||||
|
return []string{}, nil
|
||||||
|
|
||||||
|
case []string:
|
||||||
|
var formattedLines []string
|
||||||
|
for _, item := range v {
|
||||||
|
lines := strings.Split(item, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.TrimSpace(line) != "" {
|
||||||
|
formattedLines = append(formattedLines, "• "+strings.TrimSpace(line))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
joinedValue := strings.Join(formattedLines, "\n")
|
||||||
|
|
||||||
|
if len(config.Columns) == 0 {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
col := config.Columns[0]
|
||||||
|
cellRef := fmt.Sprintf("%s%d", col, row)
|
||||||
|
|
||||||
|
f.SetCellValue(sheetName, cellRef, joinedValue)
|
||||||
|
|
||||||
|
textStyleID, err := createCellStyle(f, getTextStyle())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot create text style: %w", err)
|
||||||
|
}
|
||||||
|
f.SetCellStyle(sheetName, cellRef, cellRef, textStyleID)
|
||||||
|
|
||||||
|
if isFirstRow {
|
||||||
|
width := config.DefaultWidth
|
||||||
|
if len(config.Width) > 0 {
|
||||||
|
width = config.Width[0]
|
||||||
|
}
|
||||||
|
f.SetColWidth(sheetName, col, col, width)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.HasFilter {
|
||||||
|
if len(config.FilterColumns) > 0 {
|
||||||
|
return config.FilterColumns, nil
|
||||||
|
}
|
||||||
|
return []string{col}, nil
|
||||||
|
}
|
||||||
|
return []string{}, nil
|
||||||
|
|
||||||
|
case *bool:
|
||||||
|
yesNoValue := boolToYesNo(v)
|
||||||
|
excelValue := yesNoValue.MarshalExcel()
|
||||||
|
|
||||||
|
styleID, err := createCellStyle(f, excelValue.Style)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return writeSingleColumnField(f, sheetName, row, excelValue, &config, styleID, isFirstRow)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("no handler found for field type: %T", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateExcel(data SOAData) ([]byte, error) {
|
||||||
|
return GenerateSOAExcel(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeSingleColumnField(f *excelize.File, sheetName string, row int, excelValue ExcelValue, colDef *FieldConfiguration, styleID int, isFirstRow bool) ([]string, error) {
|
||||||
|
if len(colDef.Columns) == 0 {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
col := colDef.Columns[0]
|
||||||
|
cellRef := fmt.Sprintf("%s%d", col, row)
|
||||||
|
|
||||||
|
f.SetCellValue(sheetName, cellRef, excelValue.Value)
|
||||||
|
f.SetCellStyle(sheetName, cellRef, cellRef, styleID)
|
||||||
|
|
||||||
|
if isFirstRow {
|
||||||
|
if err := applyDataValidation(f, sheetName, col, row, excelValue.Validation); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := setColumnWidth(f, sheetName, col, excelValue.Width); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if colDef.HasFilter {
|
||||||
|
if len(colDef.FilterColumns) > 0 {
|
||||||
|
return colDef.FilterColumns, nil
|
||||||
|
}
|
||||||
|
return []string{col}, nil
|
||||||
|
}
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
129
pkg/soagen/header.go
Normal file
129
pkg/soagen/header.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
package soagen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/xuri/excelize/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HeaderCell struct {
|
||||||
|
Cell string
|
||||||
|
Value string
|
||||||
|
Style string // "header" or "cell"
|
||||||
|
}
|
||||||
|
|
||||||
|
type MergedCell struct {
|
||||||
|
StartCell string
|
||||||
|
EndCell string
|
||||||
|
}
|
||||||
|
|
||||||
|
type HeaderLayout struct {
|
||||||
|
InfoHeader []HeaderCell
|
||||||
|
InfoData []HeaderCell
|
||||||
|
MainHeader []HeaderCell
|
||||||
|
SubHeader []HeaderCell
|
||||||
|
Merges []MergedCell
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSOAHeaderLayout() HeaderLayout {
|
||||||
|
return HeaderLayout{
|
||||||
|
InfoHeader: []HeaderCell{
|
||||||
|
{"A2", "Version", "header"},
|
||||||
|
{"C2", "Date", "header"},
|
||||||
|
{"D2", "Comment", "header"},
|
||||||
|
{"E2", "Author", "header"},
|
||||||
|
{"F2", "Approver", "header"},
|
||||||
|
},
|
||||||
|
InfoData: []HeaderCell{
|
||||||
|
{"A3", "1.0", "cell"},
|
||||||
|
{"C3", time.Now().Format("01/02/2006"), "cell"},
|
||||||
|
{"D3", "Initial SoA", "cell"},
|
||||||
|
{"E3", "System Admin", "cell"},
|
||||||
|
{"F3", "Security Manager", "cell"},
|
||||||
|
},
|
||||||
|
MainHeader: []HeaderCell{
|
||||||
|
{"A6", "Control", "header"},
|
||||||
|
{"B6", "Control name", "header"},
|
||||||
|
{"C6", "Applicability", "header"},
|
||||||
|
{"D6", "Justification for exclusion", "header"},
|
||||||
|
{"E6", "Justification for inclusion", "header"},
|
||||||
|
{"F6", "Justification for inclusion", "header"},
|
||||||
|
{"G6", "Justification for inclusion", "header"},
|
||||||
|
{"H6", "Justification for inclusion", "header"},
|
||||||
|
{"I6", "List of security measure or policy", "header"},
|
||||||
|
},
|
||||||
|
SubHeader: []HeaderCell{
|
||||||
|
{"A7", "Control", "header"},
|
||||||
|
{"B7", "Control name", "header"},
|
||||||
|
{"C7", "Applicability", "header"},
|
||||||
|
{"D7", "Justification for exclusion", "header"},
|
||||||
|
{"E7", "Regulatory", "header"},
|
||||||
|
{"F7", "Contractual", "header"},
|
||||||
|
{"G7", "Best practice", "header"},
|
||||||
|
{"H7", "Risk assessment", "header"},
|
||||||
|
{"I7", "List of security measure or policy", "header"},
|
||||||
|
},
|
||||||
|
Merges: []MergedCell{
|
||||||
|
{"A6", "A7"},
|
||||||
|
{"B6", "B7"},
|
||||||
|
{"C6", "C7"},
|
||||||
|
{"D6", "D7"},
|
||||||
|
{"E6", "H6"},
|
||||||
|
{"I6", "I7"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyHeaderLayout(f *excelize.File, sheetName string, layout HeaderLayout, headerStyle, cellStyle int) error {
|
||||||
|
if err := applyCells(f, sheetName, layout.InfoHeader, headerStyle, cellStyle); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := applyCells(f, sheetName, layout.InfoData, headerStyle, cellStyle); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := applyCells(f, sheetName, layout.MainHeader, headerStyle, cellStyle); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := applyCells(f, sheetName, layout.SubHeader, headerStyle, cellStyle); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, merge := range layout.Merges {
|
||||||
|
if err := f.MergeCell(sheetName, merge.StartCell, merge.EndCell); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyCells(f *excelize.File, sheetName string, cells []HeaderCell, headerStyle, cellStyle int) error {
|
||||||
|
for _, cell := range cells {
|
||||||
|
f.SetCellValue(sheetName, cell.Cell, cell.Value)
|
||||||
|
|
||||||
|
style := cellStyle
|
||||||
|
if cell.Style == "header" {
|
||||||
|
style = headerStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
f.SetCellStyle(sheetName, cell.Cell, cell.Cell, style)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
33
pkg/soagen/soa_data.go
Normal file
33
pkg/soagen/soa_data.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
package soagen
|
||||||
|
|
||||||
|
// SOARowData represents a single row in the State of Applicability Excel
|
||||||
|
type SOARowData struct {
|
||||||
|
SectionTitle string
|
||||||
|
ControlName string
|
||||||
|
Applicability Applicability
|
||||||
|
Regulatory *bool
|
||||||
|
Contractual *bool
|
||||||
|
BestPractice *bool
|
||||||
|
RiskAssessment *bool
|
||||||
|
JustificationExclusion string
|
||||||
|
SecurityMeasures []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SOAData contains all the data needed for State of Applicability generation
|
||||||
|
type SOAData struct {
|
||||||
|
Rows []SOARowData
|
||||||
|
}
|
||||||
89
pkg/soagen/styles.go
Normal file
89
pkg/soagen/styles.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
package soagen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/xuri/excelize/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getTextStyle returns the standard text style for string fields
|
||||||
|
func getTextStyle() *excelize.Style {
|
||||||
|
return &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},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getHeaderStyle returns the standard header style
|
||||||
|
func getHeaderStyle() *excelize.Style {
|
||||||
|
return &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},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCellStyle returns the standard cell style
|
||||||
|
func getCellStyle() *excelize.Style {
|
||||||
|
return &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},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createCellStyle(f *excelize.File, style *excelize.Style) (int, error) {
|
||||||
|
styleID, err := f.NewStyle(style)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("cannot create style: %w", err)
|
||||||
|
}
|
||||||
|
return styleID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyDataValidation(f *excelize.File, sheetName, col string, row int, validation []string) error {
|
||||||
|
if len(validation) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dv := excelize.NewDataValidation(true)
|
||||||
|
dv.Sqref = fmt.Sprintf("%s%d:%s1000", col, row, col) // Apply to reasonable range
|
||||||
|
dv.SetDropList(validation)
|
||||||
|
dv.SetError(excelize.DataValidationErrorStyleStop, "Invalid Input", "Please select from the dropdown list.")
|
||||||
|
return f.AddDataValidation(sheetName, dv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setColumnWidth(f *excelize.File, sheetName, col string, width float64) error {
|
||||||
|
if width > 0 {
|
||||||
|
f.SetColWidth(sheetName, col, col, width)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
60
pkg/soagen/yesno.go
Normal file
60
pkg/soagen/yesno.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
package soagen
|
||||||
|
|
||||||
|
import "github.com/xuri/excelize/v2"
|
||||||
|
|
||||||
|
// YesNo represents a YES/NO/Empty value
|
||||||
|
type YesNo string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Yes YesNo = "YES"
|
||||||
|
No YesNo = "NO"
|
||||||
|
Empty YesNo = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
// String returns the string representation of YesNo
|
||||||
|
func (yn YesNo) String() string {
|
||||||
|
return string(yn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalExcel implements ExcelMarshaler for YesNo
|
||||||
|
func (yn YesNo) MarshalExcel() ExcelValue {
|
||||||
|
return ExcelValue{
|
||||||
|
Value: yn.String(),
|
||||||
|
Style: &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},
|
||||||
|
},
|
||||||
|
Validation: []string{string(Yes), string(No), string(Empty)},
|
||||||
|
Width: 12,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// boolToYesNo converts *bool to YesNo type for Excel output
|
||||||
|
func boolToYesNo(b *bool) YesNo {
|
||||||
|
if b == nil {
|
||||||
|
return Empty
|
||||||
|
}
|
||||||
|
if *b {
|
||||||
|
return Yes
|
||||||
|
}
|
||||||
|
return No
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user