Add finding and obligation publish to document system

Replace the old snapshot-based approach with the new publish document
system for findings and obligations. Includes GraphQL mutations, MCP
tools, CLI commands, e2e tests, frontend publish dialogs, and
snapshot-to-document migration tools.

Remove snapshot mode entirely from findings and obligations: drop
snapshotId from GraphQL schemas, filters, resolvers, MCP spec, frontend
routes, pages, and helpers. The snapshot_id column remains in the
database but is now filtered out with snapshot_id IS NULL.

Remove auditor's ability to publish SoA.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-04-23 10:53:57 +02:00
parent e473884b31
commit bdb16d4abe
57 changed files with 4241 additions and 609 deletions

View File

@@ -741,11 +741,9 @@ func (r *Resolver) ListFindingsTool(ctx context.Context, req *mcp.CallToolReques
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
noSnapshot := (*gid.GID)(nil)
findingFilter := coredata.NewFindingFilter(&noSnapshot, nil, nil, nil, nil)
findingFilter := coredata.NewFindingFilter(nil, nil, nil, nil)
if input.Filter != nil {
findingFilter = coredata.NewFindingFilter(
&input.Filter.SnapshotID,
input.Filter.Kind,
input.Filter.Status,
input.Filter.Priority,
@@ -857,13 +855,7 @@ func (r *Resolver) ListObligationsTool(ctx context.Context, req *mcp.CallToolReq
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
noSnapshot := (*gid.GID)(nil)
obligationFilter := coredata.NewObligationFilter(&noSnapshot)
if input.Filter != nil {
obligationFilter = coredata.NewObligationFilter(&input.Filter.SnapshotID)
}
page, err := prb.Obligations.ListForOrganizationID(ctx, input.OrganizationID, cursor, obligationFilter)
page, err := prb.Obligations.ListForOrganizationID(ctx, input.OrganizationID, cursor)
if err != nil {
panic(fmt.Errorf("cannot list organization obligations: %w", err))
}
@@ -1610,7 +1602,7 @@ func (r *Resolver) ListControlObligationsTool(ctx context.Context, req *mcp.Call
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
obligationPage, err := prb.Obligations.ListForControlID(ctx, input.ControlID, cursor, coredata.NewObligationFilter(nil))
obligationPage, err := prb.Obligations.ListForControlID(ctx, input.ControlID, cursor)
if err != nil {
return nil, types.ListControlObligationsOutput{}, fmt.Errorf("failed to list control obligations: %w", err)
}
@@ -1740,7 +1732,7 @@ func (r *Resolver) ListRiskObligationsTool(ctx context.Context, req *mcp.CallToo
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
obligationPage, err := prb.Obligations.ListForRiskID(ctx, input.RiskID, cursor, coredata.NewObligationFilter(nil))
obligationPage, err := prb.Obligations.ListForRiskID(ctx, input.RiskID, cursor)
if err != nil {
return nil, types.ListRiskObligationsOutput{}, fmt.Errorf("failed to list risk obligations: %w", err)
}
@@ -4786,3 +4778,35 @@ func (r *Resolver) AssessVendorTool(ctx context.Context, req *mcp.CallToolReques
return nil, types.NewAssessVendorOutput(result), nil
}
func (r *Resolver) PublishFindingListTool(ctx context.Context, req *mcp.CallToolRequest, input *types.PublishFindingListInput) (*mcp.CallToolResult, types.PublishFindingListOutput, error) {
r.MustAuthorize(ctx, input.OrganizationID, probo.ActionFindingPublish)
svc := r.ProboService(ctx, input.OrganizationID)
document, documentVersion, err := svc.GeneratedDocuments.PublishFindingList(ctx, input.OrganizationID, input.ApproverIds)
if err != nil {
return nil, types.PublishFindingListOutput{}, fmt.Errorf("cannot publish finding list: %w", err)
}
return nil, types.PublishFindingListOutput{
DocumentID: document.ID,
DocumentVersionID: documentVersion.ID,
}, nil
}
func (r *Resolver) PublishObligationListTool(ctx context.Context, req *mcp.CallToolRequest, input *types.PublishObligationListInput) (*mcp.CallToolResult, types.PublishObligationListOutput, error) {
r.MustAuthorize(ctx, input.OrganizationID, probo.ActionObligationPublish)
svc := r.ProboService(ctx, input.OrganizationID)
document, documentVersion, err := svc.GeneratedDocuments.PublishObligationList(ctx, input.OrganizationID, input.ApproverIds)
if err != nil {
return nil, types.PublishObligationListOutput{}, fmt.Errorf("cannot publish obligation list: %w", err)
}
return nil, types.PublishObligationListOutput{
DocumentID: document.ID,
DocumentVersionID: documentVersion.ID,
}, nil
}

View File

@@ -2827,18 +2827,6 @@ components:
organization_id:
$ref: "#/components/schemas/GID"
description: Organization ID
snapshot_id:
anyOf:
- $ref: "#/components/schemas/GID"
description: Snapshot ID
- type: "null"
description: No snapshot
description: Snapshot ID
source_id:
type:
- string
- "null"
description: Source ID
kind:
$ref: "#/components/schemas/FindingKind"
description: Finding kind
@@ -2929,12 +2917,6 @@ components:
filter:
type: object
properties:
snapshot_id:
anyOf:
- $ref: "#/components/schemas/GID"
- type: "null"
description: Filter by snapshot ID. Defaults to null, which returns only findings with no snapshot (current live data). Pass a specific snapshot ID to retrieve findings as they were at that snapshot.
default: null
kind:
anyOf:
- $ref: "#/components/schemas/FindingKind"
@@ -3279,18 +3261,6 @@ components:
organization_id:
$ref: "#/components/schemas/GID"
description: Organization ID
snapshot_id:
anyOf:
- $ref: "#/components/schemas/GID"
description: Snapshot ID
- type: "null"
description: No snapshot
description: Snapshot ID
source_id:
type:
- string
- "null"
description: Source ID
area:
type:
- string
@@ -3363,16 +3333,6 @@ components:
cursor:
$ref: "#/components/schemas/CursorKey"
description: Page cursor
filter:
type: object
properties:
snapshot_id:
anyOf:
- $ref: "#/components/schemas/GID"
- type: "null"
description: Filter by snapshot ID. Defaults to null, which returns only obligations with no snapshot (current live data). Pass a specific snapshot ID to retrieve obligations as they were at that snapshot.
default: null
ListObligationsOutput:
type: object
required:
@@ -7051,6 +7011,60 @@ components:
$ref: "#/components/schemas/GID"
description: Created document version ID
PublishFindingListInput:
type: object
required:
- organization_id
properties:
organization_id:
$ref: "#/components/schemas/GID"
description: Organization ID
approver_ids:
type: array
items:
$ref: "#/components/schemas/GID"
description: Optional approver profile IDs. If provided, creates a draft pending approval instead of publishing immediately.
PublishFindingListOutput:
type: object
required:
- document_id
- document_version_id
properties:
document_id:
$ref: "#/components/schemas/GID"
description: Created or updated document ID
document_version_id:
$ref: "#/components/schemas/GID"
description: Created document version ID
PublishObligationListInput:
type: object
required:
- organization_id
properties:
organization_id:
$ref: "#/components/schemas/GID"
description: Organization ID
approver_ids:
type: array
items:
$ref: "#/components/schemas/GID"
description: Optional approver profile IDs. If provided, creates a draft pending approval instead of publishing immediately.
PublishObligationListOutput:
type: object
required:
- document_id
- document_version_id
properties:
document_id:
$ref: "#/components/schemas/GID"
description: Created or updated document ID
document_version_id:
$ref: "#/components/schemas/GID"
description: Created document version ID
PublishStatementOfApplicabilityInput:
type: object
required:
@@ -10288,6 +10302,22 @@ tools:
$ref: "#/components/schemas/PublishAssetListInput"
outputSchema:
$ref: "#/components/schemas/PublishAssetListOutput"
- name: publishFindingList
description: Publish the finding register for an organization as a document. If a document already exists, a new version is created.
hints:
readonly: false
inputSchema:
$ref: "#/components/schemas/PublishFindingListInput"
outputSchema:
$ref: "#/components/schemas/PublishFindingListOutput"
- name: publishObligationList
description: Publish the obligation register for an organization as a document. If a document already exists, a new version is created.
hints:
readonly: false
inputSchema:
$ref: "#/components/schemas/PublishObligationListInput"
outputSchema:
$ref: "#/components/schemas/PublishObligationListOutput"
- name: publishStatementOfApplicability
description: Publish a statement of applicability as a document. If a document already exists, a new version is created.
hints:

View File

@@ -23,7 +23,6 @@ func NewFinding(f *coredata.Finding) *Finding {
finding := &Finding{
ID: f.ID,
OrganizationID: f.OrganizationID,
SnapshotID: f.SnapshotID,
Kind: f.Kind,
ReferenceID: f.ReferenceID,
Description: f.Description,
@@ -41,11 +40,6 @@ func NewFinding(f *coredata.Finding) *Finding {
UpdatedAt: f.UpdatedAt,
}
if f.SourceID != nil {
s := f.SourceID.String()
finding.SourceID = &s
}
return finding
}

View File

@@ -23,7 +23,6 @@ func NewObligation(o *coredata.Obligation) *Obligation {
obligation := &Obligation{
ID: o.ID,
OrganizationID: o.OrganizationID,
SnapshotID: o.SnapshotID,
Area: o.Area,
Source: o.Source,
Requirement: o.Requirement,
@@ -38,11 +37,6 @@ func NewObligation(o *coredata.Obligation) *Obligation {
UpdatedAt: o.UpdatedAt,
}
if o.SourceID != nil {
s := o.SourceID.String()
obligation.SourceID = &s
}
return obligation
}