diff --git a/apps/console/src/pages/organizations/frameworks/FrameworkLayoutView.tsx b/apps/console/src/pages/organizations/frameworks/FrameworkLayoutView.tsx index 48450282d..d23196f99 100644 --- a/apps/console/src/pages/organizations/frameworks/FrameworkLayoutView.tsx +++ b/apps/console/src/pages/organizations/frameworks/FrameworkLayoutView.tsx @@ -64,7 +64,7 @@ const DeleteFrameworkMutation = graphql` const exportAuditMutation = graphql` mutation FrameworkLayoutViewExportAuditMutation($input: ExportAuditInput!) { exportAudit(input: $input) { - success + url } } `; diff --git a/apps/console/src/pages/organizations/frameworks/__generated__/FrameworkLayoutViewExportAuditMutation.graphql.ts b/apps/console/src/pages/organizations/frameworks/__generated__/FrameworkLayoutViewExportAuditMutation.graphql.ts index 1c3ace947..356fe40b9 100644 --- a/apps/console/src/pages/organizations/frameworks/__generated__/FrameworkLayoutViewExportAuditMutation.graphql.ts +++ b/apps/console/src/pages/organizations/frameworks/__generated__/FrameworkLayoutViewExportAuditMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<1517cf7236f96500239781562cff66e4>> * @lightSyntaxTransform * @nogrep */ @@ -17,7 +17,7 @@ export type FrameworkLayoutViewExportAuditMutation$variables = { }; export type FrameworkLayoutViewExportAuditMutation$data = { readonly exportAudit: { - readonly success: boolean; + readonly url: string; }; }; export type FrameworkLayoutViewExportAuditMutation = { @@ -52,7 +52,7 @@ v1 = [ "alias": null, "args": null, "kind": "ScalarField", - "name": "success", + "name": "url", "storageKey": null } ], @@ -77,16 +77,16 @@ return { "selections": (v1/*: any*/) }, "params": { - "cacheID": "a0f656b50acde2ef3c3fef73e2155e9f", + "cacheID": "0dfccdfa75539c4621ec1bf7aab05ad2", "id": null, "metadata": {}, "name": "FrameworkLayoutViewExportAuditMutation", "operationKind": "mutation", - "text": "mutation FrameworkLayoutViewExportAuditMutation(\n $input: ExportAuditInput!\n) {\n exportAudit(input: $input) {\n success\n }\n}\n" + "text": "mutation FrameworkLayoutViewExportAuditMutation(\n $input: ExportAuditInput!\n) {\n exportAudit(input: $input) {\n url\n }\n}\n" } }; })(); -(node as any).hash = "b7347f4f8e292580e056793df6c270a5"; +(node as any).hash = "28679b1a4e375eb17a72e9e236f2e2ee"; export default node; diff --git a/pkg/probo/framework_service.go b/pkg/probo/framework_service.go index 6f4524480..0fc113567 100644 --- a/pkg/probo/framework_service.go +++ b/pkg/probo/framework_service.go @@ -22,6 +22,9 @@ import ( "path/filepath" "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" @@ -247,7 +250,10 @@ func (s FrameworkService) Import( func (s FrameworkService) ExportAudit( ctx context.Context, frameworkID gid.GID, -) ([]*coredata.Control, error) { +) (string, error) { + var archivePath string + var objectKey string + err := s.svc.pg.WithConn( ctx, func(conn pg.Conn) error { @@ -387,13 +393,102 @@ func (s FrameworkService) ExportAudit( } } + 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 nil, err + return "", err } - return nil, nil + presignClient := s3.NewPresignClient(s.svc.s3) + + presignedReq, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(s.svc.bucket), + Key: aws.String(objectKey), + }, func(opts *s3.PresignOptions) { + opts.Expires = 15 * time.Minute + }) + if err != nil { + return "", fmt.Errorf("cannot generate presigned URL: %w", err) + } + + return presignedReq.URL, nil +} + +func createTarGzArchive(sourceDir, targetFile string) error { + 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 } diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index f83f7033a..e9c6f1a43 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -1738,5 +1738,5 @@ input ExportAuditInput { } type ExportAuditPayload { - success: Boolean! + url: String! } diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index 6b2aac919..2b3b5841d 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -257,7 +257,7 @@ type ComplexityRoot struct { } ExportAuditPayload struct { - Success func(childComplexity int) int + URL func(childComplexity int) int } Framework struct { @@ -1415,12 +1415,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.EvidenceEdge.Node(childComplexity), true - case "ExportAuditPayload.success": - if e.complexity.ExportAuditPayload.Success == nil { + case "ExportAuditPayload.url": + if e.complexity.ExportAuditPayload.URL == nil { break } - return e.complexity.ExportAuditPayload.Success(childComplexity), true + return e.complexity.ExportAuditPayload.URL(childComplexity), true case "Framework.controls": if e.complexity.Framework.Controls == nil { @@ -5709,7 +5709,7 @@ input ExportAuditInput { } type ExportAuditPayload { - success: Boolean! + url: String! } `, BuiltIn: false}, } @@ -12989,8 +12989,8 @@ func (ec *executionContext) fieldContext_EvidenceEdge_node(_ context.Context, fi return fc, nil } -func (ec *executionContext) _ExportAuditPayload_success(ctx context.Context, field graphql.CollectedField, obj *types.ExportAuditPayload) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_ExportAuditPayload_success(ctx, field) +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 } @@ -13003,7 +13003,7 @@ func (ec *executionContext) _ExportAuditPayload_success(ctx context.Context, fie }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return obj.Success, nil + return obj.URL, nil }) if err != nil { ec.Error(ctx, err) @@ -13015,19 +13015,19 @@ func (ec *executionContext) _ExportAuditPayload_success(ctx context.Context, fie } return graphql.Null } - res := resTmp.(bool) + res := resTmp.(string) fc.Result = res - return ec.marshalNBoolean2bool(ctx, field.Selections, res) + return ec.marshalNString2string(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_ExportAuditPayload_success(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +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 Boolean does not have child fields") + return nil, errors.New("field of type String does not have child fields") }, } return fc, nil @@ -17610,8 +17610,8 @@ func (ec *executionContext) fieldContext_Mutation_exportAudit(ctx context.Contex IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { - case "success": - return ec.fieldContext_ExportAuditPayload_success(ctx, field) + case "url": + return ec.fieldContext_ExportAuditPayload_url(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ExportAuditPayload", field.Name) }, @@ -35022,8 +35022,8 @@ func (ec *executionContext) _ExportAuditPayload(ctx context.Context, sel ast.Sel switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("ExportAuditPayload") - case "success": - out.Values[i] = ec._ExportAuditPayload_success(ctx, field, obj) + case "url": + out.Values[i] = ec._ExportAuditPayload_url(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go index b51ebd88e..74bb429f3 100644 --- a/pkg/server/api/console/v1/types/types.go +++ b/pkg/server/api/console/v1/types/types.go @@ -416,7 +416,7 @@ type ExportAuditInput struct { } type ExportAuditPayload struct { - Success bool `json:"success"` + URL string `json:"url"` } type Framework struct { diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index 7d7df0955..25b33c6f2 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -1292,13 +1292,13 @@ func (r *mutationResolver) CreateVendorRiskAssessment(ctx context.Context, input // ExportAudit is the resolver for the exportAudit field. func (r *mutationResolver) ExportAudit(ctx context.Context, input types.ExportAuditInput) (*types.ExportAuditPayload, error) { svc := GetTenantService(ctx, r.proboSvc, input.FrameworkID.TenantID()) - _, err := svc.Frameworks.ExportAudit(ctx, input.FrameworkID) + fileUrl, err := svc.Frameworks.ExportAudit(ctx, input.FrameworkID) if err != nil { panic(fmt.Errorf("cannot export audit: %w", err)) } return &types.ExportAuditPayload{ - Success: true, + URL: fileUrl, }, nil }