Add MCP audit report metadata and getAuditReportUrl tool

Adds has_report, report_filename, and report_mime_type fields to Audit type to expose report attachment status. Introduces new getAuditReportUrl tool to generate presigned download URLs for audit reports. Updates NewAudit type converter to accept optional report object for enriching metadata.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2026-03-20 15:58:27 +01:00
parent 138f24ef59
commit c2e13b7968
3 changed files with 93 additions and 13 deletions

View File

@@ -1346,11 +1346,19 @@ func (r *Resolver) GetAuditTool(ctx context.Context, req *mcp.CallToolRequest, i
audit, err := prb.Audits.Get(ctx, input.ID) audit, err := prb.Audits.Get(ctx, input.ID)
if err != nil { 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{ return nil, types.GetAuditOutput{
Audit: types.NewAudit(audit), Audit: types.NewAudit(audit, report),
}, nil }, nil
} }
@@ -1375,16 +1383,16 @@ func (r *Resolver) AddAuditTool(ctx context.Context, req *mcp.CallToolRequest, i
} }
return nil, types.AddAuditOutput{ return nil, types.AddAuditOutput{
Audit: types.NewAudit(audit), Audit: types.NewAudit(audit, nil),
}, nil }, nil
} }
func (r *Resolver) UpdateAuditTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateAuditInput) (*mcp.CallToolResult, types.UpdateAuditOutput, error) { 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) 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, ctx,
&probo.UpdateAuditRequest{ &probo.UpdateAuditRequest{
ID: input.ID, ID: input.ID,
@@ -1396,11 +1404,19 @@ func (r *Resolver) UpdateAuditTool(ctx context.Context, req *mcp.CallToolRequest
}, },
) )
if err != nil { 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{ return nil, types.UpdateAuditOutput{
Audit: types.NewAudit(audit), Audit: types.NewAudit(audit, report),
}, nil }, nil
} }
@@ -3156,7 +3172,7 @@ func (r *Resolver) LinkFindingAuditTool(ctx context.Context, req *mcp.CallToolRe
return nil, types.LinkFindingAuditOutput{ return nil, types.LinkFindingAuditOutput{
Finding: types.NewFinding(finding), Finding: types.NewFinding(finding),
Audit: types.NewAudit(audit), Audit: types.NewAudit(audit, nil),
}, nil }, nil
} }
@@ -3281,3 +3297,18 @@ func (r *Resolver) UpdateOrganizationContextTool(ctx context.Context, req *mcp.C
OrganizationContext: types.NewOrganizationContext(orgContext), OrganizationContext: types.NewOrganizationContext(orgContext),
}, nil }, 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
}

View File

@@ -3935,6 +3935,7 @@ components:
- framework_id - framework_id
- state - state
- trust_center_visibility - trust_center_visibility
- has_report
- created_at - created_at
- updated_at - updated_at
properties: properties:
@@ -3970,6 +3971,19 @@ components:
trust_center_visibility: trust_center_visibility:
$ref: "#/components/schemas/TrustCenterVisibility" $ref: "#/components/schemas/TrustCenterVisibility"
description: Trust center visibility 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: created_at:
type: string type: string
format: date-time format: date-time
@@ -3979,6 +3993,24 @@ components:
format: date-time format: date-time
description: Update timestamp 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: ListAuditsInput:
type: object type: object
required: required:
@@ -7018,6 +7050,15 @@ tools:
$ref: "#/components/schemas/UpdateAuditInput" $ref: "#/components/schemas/UpdateAuditInput"
outputSchema: outputSchema:
$ref: "#/components/schemas/UpdateAuditOutput" $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 - name: listControls
description: List all controls for the organization or framework description: List all controls for the organization or framework
hints: hints:

View File

@@ -19,25 +19,33 @@ import (
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
) )
func NewAudit(a *coredata.Audit) *Audit { func NewAudit(a *coredata.Audit, report *coredata.Report) *Audit {
return &Audit{ audit := &Audit{
ID: a.ID, ID: a.ID,
Name: a.Name, Name: a.Name,
OrganizationID: a.OrganizationID, OrganizationID: a.OrganizationID,
FrameworkID: a.FrameworkID, FrameworkID: a.FrameworkID,
State: a.State, State: a.State,
TrustCenterVisibility: a.TrustCenterVisibility, TrustCenterVisibility: a.TrustCenterVisibility,
HasReport: a.ReportID != nil,
ValidFrom: a.ValidFrom, ValidFrom: a.ValidFrom,
ValidUntil: a.ValidUntil, ValidUntil: a.ValidUntil,
CreatedAt: a.CreatedAt, CreatedAt: a.CreatedAt,
UpdatedAt: a.UpdatedAt, 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 { func NewListControlAuditsOutput(auditPage *page.Page[*coredata.Audit, coredata.AuditOrderField]) ListControlAuditsOutput {
audits := make([]*Audit, 0, len(auditPage.Data)) audits := make([]*Audit, 0, len(auditPage.Data))
for _, v := range auditPage.Data { for _, v := range auditPage.Data {
audits = append(audits, NewAudit(v)) audits = append(audits, NewAudit(v, nil))
} }
var nextCursor *page.CursorKey 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 { func NewListAuditsOutput(auditPage *page.Page[*coredata.Audit, coredata.AuditOrderField]) ListAuditsOutput {
audits := make([]*Audit, 0, len(auditPage.Data)) audits := make([]*Audit, 0, len(auditPage.Data))
for _, v := range auditPage.Data { for _, v := range auditPage.Data {
audits = append(audits, NewAudit(v)) audits = append(audits, NewAudit(v, nil))
} }
var nextCursor *page.CursorKey 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 { func NewListFindingAuditsOutput(auditPage *page.Page[*coredata.Audit, coredata.AuditOrderField]) ListFindingAuditsOutput {
audits := make([]*Audit, 0, len(auditPage.Data)) audits := make([]*Audit, 0, len(auditPage.Data))
for _, v := range auditPage.Data { for _, v := range auditPage.Data {
audits = append(audits, NewAudit(v)) audits = append(audits, NewAudit(v, nil))
} }
var nextCursor *page.CursorKey var nextCursor *page.CursorKey