diff --git a/pkg/server/api/mcp/v1/schema.resolvers.go b/pkg/server/api/mcp/v1/schema.resolvers.go index 6a67fa361..c9a1f9f32 100644 --- a/pkg/server/api/mcp/v1/schema.resolvers.go +++ b/pkg/server/api/mcp/v1/schema.resolvers.go @@ -1346,11 +1346,19 @@ func (r *Resolver) GetAuditTool(ctx context.Context, req *mcp.CallToolRequest, i audit, err := prb.Audits.Get(ctx, input.ID) if err != nil { - return nil, types.GetAuditOutput{}, fmt.Errorf("failed to get audit: %w", err) + return nil, types.GetAuditOutput{}, fmt.Errorf("cannot get audit: %w", err) + } + + var report *coredata.Report + if audit.ReportID != nil { + report, err = prb.Reports.Get(ctx, *audit.ReportID) + if err != nil { + return nil, types.GetAuditOutput{}, fmt.Errorf("cannot get audit report: %w", err) + } } return nil, types.GetAuditOutput{ - Audit: types.NewAudit(audit), + Audit: types.NewAudit(audit, report), }, nil } @@ -1375,16 +1383,16 @@ func (r *Resolver) AddAuditTool(ctx context.Context, req *mcp.CallToolRequest, i } return nil, types.AddAuditOutput{ - Audit: types.NewAudit(audit), + Audit: types.NewAudit(audit, nil), }, nil } func (r *Resolver) UpdateAuditTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateAuditInput) (*mcp.CallToolResult, types.UpdateAuditOutput, error) { r.MustAuthorize(ctx, input.ID, probo.ActionAuditUpdate) - svc := r.ProboService(ctx, input.ID) + prb := r.ProboService(ctx, input.ID) - audit, err := svc.Audits.Update( + audit, err := prb.Audits.Update( ctx, &probo.UpdateAuditRequest{ ID: input.ID, @@ -1396,11 +1404,19 @@ func (r *Resolver) UpdateAuditTool(ctx context.Context, req *mcp.CallToolRequest }, ) if err != nil { - return nil, types.UpdateAuditOutput{}, fmt.Errorf("failed to update audit: %w", err) + return nil, types.UpdateAuditOutput{}, fmt.Errorf("cannot update audit: %w", err) + } + + var report *coredata.Report + if audit.ReportID != nil { + report, err = prb.Reports.Get(ctx, *audit.ReportID) + if err != nil { + return nil, types.UpdateAuditOutput{}, fmt.Errorf("cannot get audit report: %w", err) + } } return nil, types.UpdateAuditOutput{ - Audit: types.NewAudit(audit), + Audit: types.NewAudit(audit, report), }, nil } @@ -3156,7 +3172,7 @@ func (r *Resolver) LinkFindingAuditTool(ctx context.Context, req *mcp.CallToolRe return nil, types.LinkFindingAuditOutput{ Finding: types.NewFinding(finding), - Audit: types.NewAudit(audit), + Audit: types.NewAudit(audit, nil), }, nil } @@ -3281,3 +3297,18 @@ func (r *Resolver) UpdateOrganizationContextTool(ctx context.Context, req *mcp.C OrganizationContext: types.NewOrganizationContext(orgContext), }, nil } + +func (r *Resolver) GetAuditReportUrlTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetAuditReportUrlInput) (*mcp.CallToolResult, types.GetAuditReportUrlOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionReportGetReportUrl) + + prb := r.ProboService(ctx, input.ID) + + url, err := prb.Audits.GenerateReportURL(ctx, input.ID, 15*time.Minute) + if err != nil { + return nil, types.GetAuditReportUrlOutput{}, fmt.Errorf("cannot generate audit report URL: %w", err) + } + + return nil, types.GetAuditReportUrlOutput{ + URL: *url, + }, nil +} diff --git a/pkg/server/api/mcp/v1/specification.yaml b/pkg/server/api/mcp/v1/specification.yaml index 8ca4b0aa5..8d38fef4b 100644 --- a/pkg/server/api/mcp/v1/specification.yaml +++ b/pkg/server/api/mcp/v1/specification.yaml @@ -3935,6 +3935,7 @@ components: - framework_id - state - trust_center_visibility + - has_report - created_at - updated_at properties: @@ -3970,6 +3971,19 @@ components: trust_center_visibility: $ref: "#/components/schemas/TrustCenterVisibility" description: Trust center visibility + has_report: + type: boolean + description: Whether the audit has an attached report + report_filename: + type: + - string + - "null" + description: Report filename, null if no report attached + report_mime_type: + type: + - string + - "null" + description: Report MIME type, null if no report attached created_at: type: string format: date-time @@ -3979,6 +3993,24 @@ components: format: date-time description: Update timestamp + GetAuditReportUrlInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Audit ID + + GetAuditReportUrlOutput: + type: object + required: + - url + properties: + url: + type: string + description: Presigned download URL for the report (valid for 15 minutes) + ListAuditsInput: type: object required: @@ -7018,6 +7050,15 @@ tools: $ref: "#/components/schemas/UpdateAuditInput" outputSchema: $ref: "#/components/schemas/UpdateAuditOutput" + - name: getAuditReportUrl + description: Get a presigned download URL for an audit's attached report. Returns a time-limited URL valid for 15 minutes. The audit must have an attached report. + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/GetAuditReportUrlInput" + outputSchema: + $ref: "#/components/schemas/GetAuditReportUrlOutput" - name: listControls description: List all controls for the organization or framework hints: diff --git a/pkg/server/api/mcp/v1/types/audit.go b/pkg/server/api/mcp/v1/types/audit.go index c38cbf986..3db183fb5 100644 --- a/pkg/server/api/mcp/v1/types/audit.go +++ b/pkg/server/api/mcp/v1/types/audit.go @@ -19,25 +19,33 @@ import ( "go.probo.inc/probo/pkg/page" ) -func NewAudit(a *coredata.Audit) *Audit { - return &Audit{ +func NewAudit(a *coredata.Audit, report *coredata.Report) *Audit { + audit := &Audit{ ID: a.ID, Name: a.Name, OrganizationID: a.OrganizationID, FrameworkID: a.FrameworkID, State: a.State, TrustCenterVisibility: a.TrustCenterVisibility, + HasReport: a.ReportID != nil, ValidFrom: a.ValidFrom, ValidUntil: a.ValidUntil, CreatedAt: a.CreatedAt, UpdatedAt: a.UpdatedAt, } + + if report != nil { + audit.ReportFilename = &report.Filename + audit.ReportMimeType = &report.MimeType + } + + return audit } func NewListControlAuditsOutput(auditPage *page.Page[*coredata.Audit, coredata.AuditOrderField]) ListControlAuditsOutput { audits := make([]*Audit, 0, len(auditPage.Data)) for _, v := range auditPage.Data { - audits = append(audits, NewAudit(v)) + audits = append(audits, NewAudit(v, nil)) } var nextCursor *page.CursorKey @@ -55,7 +63,7 @@ func NewListControlAuditsOutput(auditPage *page.Page[*coredata.Audit, coredata.A func NewListAuditsOutput(auditPage *page.Page[*coredata.Audit, coredata.AuditOrderField]) ListAuditsOutput { audits := make([]*Audit, 0, len(auditPage.Data)) for _, v := range auditPage.Data { - audits = append(audits, NewAudit(v)) + audits = append(audits, NewAudit(v, nil)) } var nextCursor *page.CursorKey @@ -73,7 +81,7 @@ func NewListAuditsOutput(auditPage *page.Page[*coredata.Audit, coredata.AuditOrd func NewListFindingAuditsOutput(auditPage *page.Page[*coredata.Audit, coredata.AuditOrderField]) ListFindingAuditsOutput { audits := make([]*Audit, 0, len(auditPage.Data)) for _, v := range auditPage.Data { - audits = append(audits, NewAudit(v)) + audits = append(audits, NewAudit(v, nil)) } var nextCursor *page.CursorKey