Skip to content
4 changes: 4 additions & 0 deletions docs/scanner-imports/schema-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ With the SDK, repeat the preparation call that created the submission, using
the same options. For `prepareExternalPublication()`, supply the original
export file. For `prepareNormalizedExternalPublication()`, supply the original
`ExternalFindings`. Check `preview.resumed` before calling `publish()`.
For stored findings, reprepare the interrupted source group with its saved selection
and review ID. Its checkpoint belongs to the selected checkout and finding versions.
After a repository reset, the workbench records any acknowledged receipts before
retiring the old request. Review the new destination before publishing again.

**Upgrading from an earlier import client.** Finish pending Cloud submissions with
the CLI or SDK build that created their checkpoints. Keep the original input and
Expand Down
32 changes: 32 additions & 0 deletions examples/normalized-import/guardian-response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"isError": false,
"content": [],
"structuredContent": {
"findings": [
{
"id": "9007199254740992",
"repository": { "name": "example/project" },
"ref": "refs/heads/main",
"issueType": "sast",
"rulePath": "demo.other-rule",
"message": "Another finding in the tool response",
"severity": "LOW",
"status": "open",
"filePath": "src/other.ts",
"line": 3
},
{
"id": "9007199254740993",
"repository": { "name": "example/project" },
"ref": "refs/heads/main",
"issueType": "sast",
"rulePath": "demo.unchecked-path",
"message": "A user-supplied path reaches the file reader",
"severity": "HIGH",
"status": "open",
"filePath": "src/read.ts",
"line": 12
}
]
}
}
13 changes: 13 additions & 0 deletions examples/normalized-import/guardian-selection.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"sourceKey": "demo/guardian/example/project/main/sast",
"sourceRepositoryUrl": "https://github.com/example/project",
"request": {
"name": "get_semgrep_sast_findings",
"arguments": {
"deployment_name": "demo",
"repository_name": "example/project",
"ref": "refs/heads/main"
}
},
"findingIds": ["9007199254740993"]
}
33 changes: 33 additions & 0 deletions examples/normalized-import/trivy-report.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"SchemaVersion": 2,
"Trivy": { "Version": "0.76.1" },
"ReportID": "00000000-0000-7000-8000-000000000001",
"CreatedAt": "2026-01-01T00:00:00Z",
"ArtifactType": "repository",
"ArtifactName": "https://github.com/example/project",
"Metadata": {
"RepoURL": "https://github.com/example/project",
"Branch": "main",
"Commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"Results": [
{
"Target": "package-lock.json",
"Class": "lang-pkgs",
"Type": "npm",
"Vulnerabilities": [
{
"VulnerabilityID": "CVE-2099-0001",
"PkgName": "@example/parser",
"PkgID": "@example/parser@1.0.0",
"InstalledVersion": "1.0.0",
"FixedVersion": "1.0.1",
"Title": "Example parser accepts an invalid length",
"Description": "A crafted length can cause excessive allocation.",
"Severity": "HIGH",
"PrimaryURL": "https://advisories.example.invalid/CVE-2099-0001"
}
]
}
]
}
10 changes: 8 additions & 2 deletions plugins/codex-security/mcp-app/helpers-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,14 @@ function runHelper(): void {
"Usage: list-stored-findings\nReads a JSON object from stdin with an absolute stateDirectory, positive payload.limit and non-negative payload.offset.",
"store-external-findings":
"Usage: store-external-findings\nReads a JSON object from stdin with an absolute stateDirectory and payload containing repository, provider, sourceKey and findings with source_finding_id and evidence.",
"list-external-findings":
"Usage: list-external-findings\nReads a JSON object from stdin with an absolute stateDirectory and payload.repository; payload.id optionally selects one external finding.",
"read-external-findings":
"Usage: read-external-findings\nReads a JSON object from stdin with an absolute stateDirectory and payload.repository. Select an id, an importId, or a list of findings with id and version.",
"list-external-imports":
"Usage: list-external-imports\nReads a JSON object from stdin with an absolute stateDirectory and payload.repository.",
"record-external-finding-publications":
"Usage: record-external-finding-publications\nReads a JSON object from stdin with an absolute stateDirectory and publication receipts for stored finding versions.",
"list-external-finding-publications":
"Usage: list-external-finding-publications\nReads a JSON object from stdin with an absolute stateDirectory and payload.repository. Select an optional finding id.",
"find-potential-duplicates":
"Usage: find-potential-duplicates\nReads a JSON object from stdin with an absolute stateDirectory, payload.findingId and payload.scope containing either repositoryId or allRepositories: true.",
"store-dedupe-groups":
Expand Down
23 changes: 22 additions & 1 deletion plugins/codex-security/mcp-app/src/workbench/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,16 @@ import {
} from "./duplicates";
import {
listExternalFindings,
listExternalImports,
storeExternalFindings,
type ExternalFindingSelection,
type ExternalFindingsImport,
} from "./external-findings";
import {
listExternalFindingPublications,
recordExternalFindingPublications,
type ExternalPublicationLinks,
} from "./external-finding-publications";

import {
readSeverityClassification,
Expand Down Expand Up @@ -84,6 +91,7 @@ export async function workbenchCommand(
command !== "local-dedupe" &&
command !== "store-findings" &&
command !== "store-external-findings" &&
command !== "record-external-finding-publications" &&
command !== "store-dedupe-groups" &&
command !== "severity-classification",
},
Expand All @@ -101,8 +109,21 @@ export async function workbenchCommand(
payload as ExternalFindingsImport,
new Date().toISOString(),
);
if (command === "list-external-findings")
if (command === "read-external-findings")
return listExternalFindings(
database,
payload as ExternalFindingSelection,
);
if (command === "list-external-imports")
return listExternalImports(database, payload as { repository: string });
if (command === "record-external-finding-publications")
return recordExternalFindingPublications(
database,
payload as ExternalPublicationLinks,
new Date().toISOString(),
);
if (command === "list-external-finding-publications")
return listExternalFindingPublications(
database,
payload as { repository: string; id?: string },
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type { DatabaseSync } from "node:sqlite";
import { requireRepository, requireText } from "./external-findings";
import { transaction } from "./transaction";

export interface ExternalPublicationLinks {
repository: string;
cloudApiUrl: string;
accountId: string;
destinationId: string;
entries: {
id: string;
version: number;
receiptId: string;
outcome: "created" | "updated" | "unchanged" | "private_job";
cloudFindingId?: string;
}[];
}

export function recordExternalFindingPublications(
database: DatabaseSync,
input: ExternalPublicationLinks,
timestamp: string,
) {
requireRepository(input?.repository);
requireText(input.cloudApiUrl, "cloudApiUrl");
requireText(input.accountId, "accountId");
requireText(input.destinationId, "destinationId");
if (!Array.isArray(input.entries))
throw new TypeError("entries must be an array.");
return transaction(database, "BEGIN IMMEDIATE", () => {
// The reviewed version may have been superseded during upload. Every
// positive revision through the current version existed in this repository.
const lookup = database.prepare(
"SELECT 1 FROM external_source_reports WHERE id = ? AND repository = ? AND version >= ?",
);
const insert = database.prepare(
`INSERT INTO external_finding_publications
(finding_id, version, cloud_api_url, account_id, destination_id, receipt_id, outcome, cloud_finding_id, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT DO NOTHING`,
);
let recorded = 0;
for (const {
id,
version,
receiptId,
outcome,
cloudFindingId,
} of input.entries) {
requireText(id, "id");
requireText(receiptId, "receiptId");
if (cloudFindingId !== undefined)
requireText(cloudFindingId, "cloudFindingId");
if (
!Number.isSafeInteger(version) ||
version < 1 ||
!lookup.get(id, input.repository, version)
)
throw new Error(
`No finding ${id} at version ${version} in the selected repository.`,
);
if (!["created", "updated", "unchanged", "private_job"].includes(outcome))
throw new TypeError("Unknown external publication outcome.");
recorded += Number(
insert.run(
id,
version,
input.cloudApiUrl,
input.accountId,
input.destinationId,
receiptId,
outcome,
cloudFindingId ?? null,
timestamp,
).changes,
);
}
return { recorded };
});
}

export function listExternalFindingPublications(
database: DatabaseSync,
input: { repository: string; id?: string },
) {
requireRepository(input?.repository);
if (input.id !== undefined) requireText(input.id, "id");
const query = database.prepare(
`SELECT p.finding_id AS id, p.version, p.cloud_api_url AS cloudApiUrl, p.account_id AS accountId,
p.destination_id AS destinationId, p.receipt_id AS receiptId, p.outcome, p.cloud_finding_id AS cloudFindingId,
p.created_at AS createdAt FROM external_finding_publications p
JOIN external_source_reports f ON f.id = p.finding_id WHERE f.repository = ?
${input.id === undefined ? "" : "AND p.finding_id = ?"}
ORDER BY p.created_at DESC, p.receipt_id, p.finding_id`,
);
const rows =
input.id === undefined
? query.all(input.repository)
: query.all(input.repository, input.id);
return {
publications: rows.map(({ cloudFindingId, ...row }) => ({
...row,
...(cloudFindingId == null ? {} : { cloudFindingId }),
})),
};
}
Loading
Loading