From bdb16d4abe3438bddf6a10a359795dd1d0ead170 Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Thu, 23 Apr 2026 10:53:57 +0200 Subject: [PATCH] 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 --- .../obligations/LinkedObligationsCard.tsx | 7 +- .../src/hooks/graph/ObligationGraph.ts | 13 +- .../findings/FindingDetailsPage.tsx | 52 +- .../organizations/findings/FindingsPage.tsx | 82 ++- .../findings/FindingsPageLoader.tsx | 9 +- .../dialogs/PublishFindingListDialog.tsx | 159 ++++ .../obligations/ObligationDetailsPage.tsx | 44 +- .../obligations/ObligationsPage.tsx | 72 +- .../dialogs/PublishObligationListDialog.tsx | 159 ++++ apps/console/src/routes/findingRoutes.ts | 16 - apps/console/src/routes/obligationRoutes.ts | 32 - .../main.go | 487 +++++++++++++ .../main.go | 450 ++++++++++++ e2e/console/finding_publish_test.go | 400 +++++++++++ e2e/console/obligation_publish_test.go | 400 +++++++++++ packages/helpers/src/snapshots.ts | 14 - .../nodes/Probo/actions/finding/index.ts | 9 + .../actions/finding/publish.operation.ts | 101 +++ .../nodes/Probo/actions/obligation/index.ts | 10 +- .../actions/obligation/publish.operation.ts | 101 +++ pkg/cmd/finding/finding.go | 2 + pkg/cmd/finding/publish/publish.go | 147 ++++ pkg/cmd/obligation/list/list.go | 4 +- pkg/cmd/obligation/obligation.go | 2 + pkg/cmd/obligation/publish/publish.go | 147 ++++ pkg/coredata/finding.go | 258 ++++--- pkg/coredata/finding_filter.go | 37 +- pkg/coredata/migrations/20260422T130000Z.sql | 17 + pkg/coredata/obligation.go | 211 ++++-- pkg/coredata/obligation_filter.go | 54 -- pkg/coredata/snapshots_type.go | 2 - pkg/coredata/snapshottable.go | 4 - pkg/docgen/generator.go | 43 ++ pkg/probo/actions.go | 12 +- pkg/probo/document_service.go | 10 + pkg/probo/generated_document_service.go | 680 ++++++++++++++++++ pkg/probo/obligation_service.go | 15 +- pkg/probo/policies.go | 4 - pkg/probo/templates/finding_list.json.tmpl | 101 +++ pkg/probo/templates/obligation_list.json.tmpl | 93 +++ pkg/server/api/console/v1/audit_resolvers.go | 33 +- .../api/console/v1/control_resolvers.go | 12 +- .../api/console/v1/graphql/audit.graphql | 17 +- .../api/console/v1/graphql/control.graphql | 1 - .../api/console/v1/graphql/obligation.graphql | 19 +- .../console/v1/graphql/organization.graphql | 7 +- .../api/console/v1/graphql/risk.graphql | 1 - .../api/console/v1/graphql/snapshot.graphql | 8 - .../api/console/v1/obligation_resolvers.go | 37 +- .../api/console/v1/organization_resolvers.go | 64 +- pkg/server/api/console/v1/risk_resolvers.go | 11 +- pkg/server/api/console/v1/types/finding.go | 3 +- pkg/server/api/console/v1/types/obligation.go | 7 +- pkg/server/api/mcp/v1/schema.resolvers.go | 48 +- pkg/server/api/mcp/v1/specification.yaml | 110 +-- pkg/server/api/mcp/v1/types/finding.go | 6 - pkg/server/api/mcp/v1/types/obligation.go | 6 - 57 files changed, 4241 insertions(+), 609 deletions(-) create mode 100644 apps/console/src/pages/organizations/findings/dialogs/PublishFindingListDialog.tsx create mode 100644 apps/console/src/pages/organizations/obligations/dialogs/PublishObligationListDialog.tsx create mode 100644 cmd/migrate-finding-snapshots-to-documents/main.go create mode 100644 cmd/migrate-obligation-snapshots-to-documents/main.go create mode 100644 e2e/console/finding_publish_test.go create mode 100644 e2e/console/obligation_publish_test.go create mode 100644 packages/n8n-node/nodes/Probo/actions/finding/publish.operation.ts create mode 100644 packages/n8n-node/nodes/Probo/actions/obligation/publish.operation.ts create mode 100644 pkg/cmd/finding/publish/publish.go create mode 100644 pkg/cmd/obligation/publish/publish.go create mode 100644 pkg/coredata/migrations/20260422T130000Z.sql delete mode 100644 pkg/coredata/obligation_filter.go create mode 100644 pkg/probo/templates/finding_list.json.tmpl create mode 100644 pkg/probo/templates/obligation_list.json.tmpl diff --git a/apps/console/src/components/obligations/LinkedObligationsCard.tsx b/apps/console/src/components/obligations/LinkedObligationsCard.tsx index 575c1479e..2c7037613 100644 --- a/apps/console/src/components/obligations/LinkedObligationsCard.tsx +++ b/apps/console/src/components/obligations/LinkedObligationsCard.tsx @@ -36,7 +36,6 @@ import { import { clsx } from "clsx"; import { useMemo, useState } from "react"; import { useFragment } from "react-relay"; -import { useParams } from "react-router"; import { graphql } from "relay-runtime"; import type { LinkedObligationsCardFragment$key } from "#/__generated__/core/LinkedObligationsCardFragment.graphql"; @@ -203,16 +202,12 @@ function ObligationRow(props: { const { __ } = useTranslate(); const obligation = useFragment(linkedObligationFragment, props.obligation); const organizationId = useOrganizationId(); - const { snapshotId } = useParams<{ snapshotId?: string }>(); - const isSnapshotMode = Boolean(snapshotId); const onDetach = () => { props.onClick(obligation.id); }; - const detailsUrl = isSnapshotMode - ? `/organizations/${organizationId}/snapshots/${snapshotId}/obligations/${obligation.id}` - : `/organizations/${organizationId}/obligations/${obligation.id}`; + const detailsUrl = `/organizations/${organizationId}/obligations/${obligation.id}`; return ( diff --git a/apps/console/src/hooks/graph/ObligationGraph.ts b/apps/console/src/hooks/graph/ObligationGraph.ts index 975956df0..ebdc973d8 100644 --- a/apps/console/src/hooks/graph/ObligationGraph.ts +++ b/apps/console/src/hooks/graph/ObligationGraph.ts @@ -25,11 +25,18 @@ import { useMutationWithToasts } from "../useMutationWithToasts"; export const ObligationsConnectionKey = "ObligationsPage_obligations"; export const obligationsQuery = graphql` - query ObligationGraphListQuery($organizationId: ID!, $snapshotId: ID) { + query ObligationGraphListQuery($organizationId: ID!) { node(id: $organizationId) { ... on Organization { canCreateObligation: permission(action: "core:obligation:create") - ...ObligationsPageFragment @arguments(snapshotId: $snapshotId) + canPublishObligations: permission(action: "core:obligation:publish") + obligationsDocument { + id + defaultApprovers { + id + } + } + ...ObligationsPageFragment } } } @@ -40,8 +47,6 @@ export const obligationNodeQuery = graphql` node(id: $obligationId) { ... on Obligation { id - snapshotId - sourceId area source requirement diff --git a/apps/console/src/pages/organizations/findings/FindingDetailsPage.tsx b/apps/console/src/pages/organizations/findings/FindingDetailsPage.tsx index be062e2e2..5787d9d85 100644 --- a/apps/console/src/pages/organizations/findings/FindingDetailsPage.tsx +++ b/apps/console/src/pages/organizations/findings/FindingDetailsPage.tsx @@ -20,7 +20,6 @@ import { getStatusVariant, type GraphQLError, sprintf, - validateSnapshotConsistency, } from "@probo/helpers"; import { useTranslate } from "@probo/i18n"; import { @@ -48,7 +47,6 @@ import { useMutation, usePreloadedQuery, } from "react-relay"; -import { useParams } from "react-router"; import { z } from "zod"; import type { FindingDetailsPageDeleteMutation } from "#/__generated__/core/FindingDetailsPageDeleteMutation.graphql"; @@ -56,7 +54,6 @@ import type { FindingDetailsPageQuery } from "#/__generated__/core/FindingDetail import type { FindingDetailsPageUpdateMutation } from "#/__generated__/core/FindingDetailsPageUpdateMutation.graphql"; import { ControlledField } from "#/components/form/ControlledField"; import { PeopleSelectField } from "#/components/form/PeopleSelectField"; -import { SnapshotBanner } from "#/components/SnapshotBanner"; import { useFormWithSchema } from "#/hooks/useFormWithSchema"; import { useOrganizationId } from "#/hooks/useOrganizationId"; @@ -67,7 +64,6 @@ export const findingDetailsPageQuery = graphql` node(id: $findingId) { ... on Finding { id - snapshotId kind referenceId description @@ -166,12 +162,8 @@ export default function FindingDetailsPage(props: Props) { const { __ } = useTranslate(); const { toast } = useToast(); const organizationId = useOrganizationId(); - const { snapshotId } = useParams<{ snapshotId?: string }>(); - const isSnapshotMode = Boolean(snapshotId); const confirm = useConfirm(); - validateSnapshotConsistency(finding, snapshotId); - const [updateFinding] = useMutation(updateFindingMutation); const [deleteFinding] = useMutation(deleteFindingMutation); @@ -181,7 +173,6 @@ export default function FindingDetailsPage(props: Props) { FindingsConnectionKey, { filter: { - snapshotId: snapshotId || null, kind: null, status: null, priority: null, @@ -194,7 +185,6 @@ export default function FindingDetailsPage(props: Props) { FindingsConnectionKey, { filter: { - snapshotId: snapshotId || null, kind: finding.kind, status: null, priority: null, @@ -322,15 +312,10 @@ export default function FindingDetailsPage(props: Props) { { value: "HIGH", label: __("High") }, ]; - const breadcrumbFindingsUrl = isSnapshotMode - ? `/organizations/${organizationId}/snapshots/${snapshotId}/findings` - : `/organizations/${organizationId}/findings`; + const breadcrumbFindingsUrl = `/organizations/${organizationId}/findings`; return (
- {isSnapshotMode && snapshotId && ( - - )}
- {!isSnapshotMode && ( - - {finding.canDelete && ( - - {__("Delete")} - - )} - - )} + + {finding.canDelete && ( + + {__("Delete")} + + )} +
@@ -393,7 +376,6 @@ export default function FindingDetailsPage(props: Props) { {...register("description")} placeholder={__("Enter description")} rows={3} - disabled={isSnapshotMode} /> @@ -405,7 +387,6 @@ export default function FindingDetailsPage(props: Props) { @@ -416,7 +397,6 @@ export default function FindingDetailsPage(props: Props) { label={__("Owner")} error={formState.errors.ownerId?.message} optional - disabled={isSnapshotMode} />
@@ -427,7 +407,6 @@ export default function FindingDetailsPage(props: Props) { type="select" label={__("Status")} required - disabled={isSnapshotMode} > {statusOptions.map(option => (