From c257b925b9efe418e6211cb702a1eb4271dde3ca Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Sat, 10 Oct 2026 23:42:16 +0000 Subject: [PATCH 1/2] refactor: own compact artifact contracts in Zod --- CONTRIBUTING.md | 7 + plugins/codex-security/mcp-app/package.json | 4 +- .../scripts/generate-artifact-schemas.mjs | 94 ++++ .../mcp-app/src/artifact-candidate-ledger.ts | 102 +--- .../mcp-app/src/artifact-contracts.ts | 529 ++++++++++++++++++ .../src/artifact-deep-reducer-pages.ts | 9 +- .../mcp-app/src/artifact-deep-reducer.ts | 31 +- .../mcp-app/src/artifact-discovery.ts | 85 +-- .../mcp-app/src/artifact-inventory.ts | 36 +- .../mcp-app/src/artifact-scan-draft.ts | 173 +++--- .../mcp-app/src/artifact-schema-loader.ts | 135 ----- .../src/deep-scan/artifact-contracts.ts | 11 +- .../src/deep-scan/artifact-validation.ts | 4 +- .../mcp-app/src/deep-scan/coordinator.ts | 2 +- .../mcp-app/src/discovery-location-roles.ts | 9 + .../mcp-app/src/finding-detail-schemas.ts | 119 ++++ .../src/helpers/normalize-candidates.ts | 11 +- .../mcp-app/src/threat-model-schema.ts | 41 ++ .../mcp-app/tests/scan-draft-fixture.ts | 6 +- .../tests/scan-draft-recovery-fixture.ts | 14 +- .../mcp-app/tests/test_artifact_contracts.ts | 158 ++++++ .../mcp-app/tests/test_artifact_foundation.ts | 54 +- .../mcp-app/tests/test_artifact_scan_draft.ts | 8 +- .../test_artifact_scan_draft_recovery.ts | 3 +- .../test_deep_scan_artifact_validation.ts | 5 +- .../tests/test_deep_scan_coordinator.ts | 3 +- .../definitions/artifact-common.schema.json | 11 +- .../discovery-candidate.schema.json | 81 ++- .../tools/candidate-attack-paths.schema.json | 146 +++-- .../tools/candidate-validations.schema.json | 82 +-- .../schemas/tools/deep-reducer.schema.json | 7 +- .../tools/discovery-candidates.schema.json | 11 +- .../schemas/tools/review-items.schema.json | 11 +- .../schemas/tools/scan-draft.schema.json | 406 ++++++-------- 34 files changed, 1506 insertions(+), 902 deletions(-) create mode 100644 plugins/codex-security/mcp-app/scripts/generate-artifact-schemas.mjs create mode 100644 plugins/codex-security/mcp-app/src/artifact-contracts.ts delete mode 100644 plugins/codex-security/mcp-app/src/artifact-schema-loader.ts create mode 100644 plugins/codex-security/mcp-app/src/discovery-location-roles.ts create mode 100644 plugins/codex-security/mcp-app/src/finding-detail-schemas.ts create mode 100644 plugins/codex-security/mcp-app/src/threat-model-schema.ts create mode 100644 plugins/codex-security/mcp-app/tests/test_artifact_contracts.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 83335097db..72383e5df5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,6 +15,13 @@ packages and releases. Do not edit or commit files in that directory. See the [SDK testing guide](sdk/typescript/TESTING.md) for the generation and validation commands. +Compact artifact contracts are authored in `plugins/codex-security/mcp-app/src/artifact-contracts.ts`, +with shared finding and threat-model definitions in `finding-detail-schemas.ts` and +`threat-model-schema.ts`. The JSON files under `schemas/definitions` and `schemas/tools` +are generated compatibility resources for JSON consumers. After editing the contracts, +run `node plugins/codex-security/mcp-app/scripts/generate-artifact-schemas.mjs`. +The MCP `typecheck` command checks that the generated resources are current. + Model-based evaluations live in [`evals/`](evals/README.md), outside the plugin source. Deterministic triage checks and the MCP reducer IPC regression remain part of normal CI. diff --git a/plugins/codex-security/mcp-app/package.json b/plugins/codex-security/mcp-app/package.json index 9b9c252529..ebca304323 100644 --- a/plugins/codex-security/mcp-app/package.json +++ b/plugins/codex-security/mcp-app/package.json @@ -4,10 +4,10 @@ "type": "module", "private": true, "scripts": { - "build": "tsc --noEmit", + "build": "node scripts/generate-artifact-schemas.mjs && tsc --noEmit", "build:mcp": "node scripts/build_mcp_app.mjs --output .preview/mcp", "test:mcp": "node --experimental-strip-types --test --test-concurrency=2 --test-reporter=./scripts/test_reporter.mts \"tests/test_*.ts\"", - "typecheck": "tsc --noEmit" + "typecheck": "node scripts/generate-artifact-schemas.mjs --check && tsc --noEmit" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.32.0", diff --git a/plugins/codex-security/mcp-app/scripts/generate-artifact-schemas.mjs b/plugins/codex-security/mcp-app/scripts/generate-artifact-schemas.mjs new file mode 100644 index 0000000000..1ada324025 --- /dev/null +++ b/plugins/codex-security/mcp-app/scripts/generate-artifact-schemas.mjs @@ -0,0 +1,94 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { build } from "esbuild"; +import { format } from "prettier"; +import * as z from "zod/v4"; + +const bundled = await build({ + stdin: { + contents: `export * from "../src/artifact-contracts.ts";`, + resolveDir: fileURLToPath(new URL(".", import.meta.url)), + }, + bundle: true, + platform: "node", + format: "esm", + write: false, +}); +const { artifactSchemaDocuments, artifactJsonMetadata } = await import( + `data:text/javascript;base64,${Buffer.from(bundled.outputFiles[0].contents).toString("base64")}` +); +const registry = z.registry(); +const documentId = (path) => `codex-security://schemas/${path}.schema.json`; +for (const [path, { $defs }] of Object.entries(artifactSchemaDocuments)) { + for (const [name, schema] of Object.entries($defs)) { + if (!schema._zod || registry.has(schema)) continue; + const id = `${documentId(path)}#/$defs/${name}`; + registry.add(schema, { id }); + } +} +const artifactJsonOptions = { + io: "input", + override: ({ zodSchema, jsonSchema }) => { + if (jsonSchema.const !== undefined || jsonSchema.enum) + delete jsonSchema.type; + Object.assign(jsonSchema, artifactJsonMetadata.get(zodSchema)); + if (JSON.stringify(jsonSchema.additionalProperties) === "{}") + jsonSchema.additionalProperties = true; + }, +}; +const { schemas } = z.toJSONSchema(registry, { + ...artifactJsonOptions, + uri: (id) => id, +}); +function body({ $schema, $id, ...schema }) { + return schema; +} +function definition(schema, id) { + const registeredId = registry.get(schema)?.id; + if (registeredId !== id) return { $ref: registeredId }; + return body(schemas[id] ?? z.toJSONSchema(schema, artifactJsonOptions)); +} +for (const [path, { $defs, ...header }] of Object.entries( + artifactSchemaDocuments, +)) { + const id = documentId(path); + const definitions = Object.fromEntries( + Object.entries($defs).map(([name, schema]) => [ + name, + schema._zod + ? definition(schema, `${id}#/$defs/${name}`) + : // attackPathFields is a field map retained for existing JSON pointers. + Object.fromEntries( + Object.entries(schema).map(([field, value]) => [ + field, + definition(value), + ]), + ), + ]), + ); + await save( + path, + { + $schema: "https://json-schema.org/draft/2020-12/schema", + $id: id, + ...header, + $defs: definitions, + }, + (_key, schema) => + JSON.stringify(schema) === '{"not":{}}' ? false : schema, + ); +} +async function save(path, schema, replacer) { + const contents = await format(JSON.stringify(schema, replacer), { + parser: "json", + }); + const output = new URL(`../../schemas/${path}.schema.json`, import.meta.url); + if (process.argv.includes("--check")) { + if ((await readFile(output, "utf8")).replaceAll("\r\n", "\n") !== contents) + throw new Error( + `Artifact schema ${path} is out of date. Run node scripts/generate-artifact-schemas.mjs from mcp-app.`, + ); + } else { + await writeFile(output, contents); + } +} diff --git a/plugins/codex-security/mcp-app/src/artifact-candidate-ledger.ts b/plugins/codex-security/mcp-app/src/artifact-candidate-ledger.ts index e811f7797b..215d39931b 100644 --- a/plugins/codex-security/mcp-app/src/artifact-candidate-ledger.ts +++ b/plugins/codex-security/mcp-app/src/artifact-candidate-ledger.ts @@ -1,8 +1,5 @@ import { asRecord } from "./record.js"; import type * as z from "zod/v4"; -import commonSchema from "../../schemas/definitions/artifact-common.schema.json"; -import validationSchema from "../../schemas/tools/candidate-validations.schema.json"; -import attackPathSchema from "../../schemas/tools/candidate-attack-paths.schema.json"; import { candidateSchemaV1 } from "./deep-scan/artifact-contracts.js"; import { artifactDestination, @@ -10,48 +7,18 @@ import { replaceArtifactJsonl, type ArtifactContext, } from "./artifact-io.js"; -import { - loadArtifactZodSchema, - type SchemaDocument, -} from "./artifact-schema-loader.js"; - -const documents = [ - commonSchema, - validationSchema, - attackPathSchema, -] as SchemaDocument[]; - -export interface CandidateValidationRecord { - disposition: "reportable" | "suppressed" | "not_applicable" | "deferred"; - method: string; - confidence: "high" | "medium" | "low"; - confidence_rationale: string; - rubric: - string | Record | Array>; - evidence: string | string[]; - counterevidence_or_proof_gap: string; - remaining_uncertainty: string; - artifact_paths?: string[]; - source?: string; - control?: string; - sink?: string; - preconditions?: string | string[]; - [field: string]: unknown; -} - -interface CandidateValidationUpdates { - validations: { - candidateId: string; - validation: CandidateValidationRecord; - }[]; -} - -/** Public workbench input; the bound context, never the caller, selects the artifact. */ -export const candidateValidationsInputSchema = loadArtifactZodSchema( - documents, - validationSchema.$id, - "input", -) as z.ZodType; +import { artifactSchemaDocuments } from "./artifact-contracts.js"; + +const validationDefinitions = + artifactSchemaDocuments["tools/candidate-validations"].$defs; +export const candidateValidationsInputSchema = validationDefinitions.input; +export type CandidateValidationRecord = z.infer< + typeof validationDefinitions.validationRecord +>; +type CandidateValidationUpdates = Pick< + z.infer, + "validations" +>; /** Complete one existing validation phase without changing discovery or attack-path data. */ export async function recordCodexSecurityCandidateValidations( @@ -109,41 +76,16 @@ export async function recordCodexSecurityCandidateValidations( }; } -type ImpactLevel = "high" | "medium" | "low" | "ignore" | "unknown"; -type ReportableSeverity = "critical" | "high" | "medium" | "low"; - -export type CandidateAttackPathRecord = { - dataflow: string; - reachability: string; - counterevidence: string; - impact: ImpactLevel; - likelihood: ImpactLevel; - severity_rationale: string; - change_conditions: string; - [field: string]: unknown; -} & ( - | { decision: "reportable"; severity: ReportableSeverity } - | { decision: "ignore"; severity: "ignore" } - | { - decision: "deferred"; - severity: ReportableSeverity | "unknown"; - proof_gap: string; - } -); - -interface CandidateAttackPathsPayload { - attackPaths: { - candidateId: string; - attackPath: CandidateAttackPathRecord; - }[]; -} - -/** The checked-in public schema controls both tools/list and call validation. */ -export const candidateAttackPathsInputSchema = loadArtifactZodSchema( - documents, - attackPathSchema.$id, - "input", -) as z.ZodType; +const attackPathDefinitions = + artifactSchemaDocuments["tools/candidate-attack-paths"].$defs; +export const candidateAttackPathsInputSchema = attackPathDefinitions.input; +export type CandidateAttackPathRecord = z.infer< + typeof attackPathDefinitions.attackPath +>; +type CandidateAttackPathsPayload = Pick< + z.infer, + "attackPaths" +>; /** Add attack-path judgments to eligible canonical Deep candidate rows. */ export async function recordCodexSecurityCandidateAttackPaths( diff --git a/plugins/codex-security/mcp-app/src/artifact-contracts.ts b/plugins/codex-security/mcp-app/src/artifact-contracts.ts new file mode 100644 index 0000000000..1c26381651 --- /dev/null +++ b/plugins/codex-security/mcp-app/src/artifact-contracts.ts @@ -0,0 +1,529 @@ +import { threatModelSchemas } from "./threat-model-schema.ts"; +import { discoveryLocationRoles } from "./discovery-location-roles.js"; +import * as z from "zod/v4"; +import { findingDetailSchemas } from "./finding-detail-schemas.ts"; + +// JSON consumers historically apply formats and integer bounds differently from +// live tools. Keep compatibility-export metadata separate from tools/list. +export const artifactJsonMetadata = z.registry>(); +function integer(minimum: number, maximum?: number) { + const schema = z.number().int().min(minimum); + return (maximum === undefined ? schema : schema.max(maximum)).register( + artifactJsonMetadata, + { maximum }, + ); +} + +function pattern(schema: z.ZodString, source: string) { + return schema.regex(new RegExp(source)).register(artifactJsonMetadata, { + pattern: source, + }); +} + +const nonEmptyText = z.string().min(1).regex(/\S/); + +const scanId = z + .string() + .check(z.uuid()) + .register(artifactJsonMetadata, { pattern: undefined }); + +const repositoryPath = pattern( + z.string().min(1).max(4096), + "^(?:\\./)?(?!/)(?![A-Za-z]:)(?!.*\\\\)(?!.*(?:^|/)\\.\\.?(/|$))(?!.*//)(?!.*[\\u0000-\\u001f\\u007f])(?!.*\\/$).+$", +); + +const candidateId = z + .string() + .min(1) + .max(512) + .regex( + new RegExp( + "^(?!\\.{1,2}$)(?!.*[\\u0000-\\u001f\\u007f])(?=.*\\S)[^/\\\\]+$", + ), + ); + +const page = z.strictObject({ + cursor: z.string().regex(new RegExp("^(?:0|[1-9][0-9]*)$")).optional(), + limit: integer(1, 1000).optional(), +}); + +const nonEmptyTextWithoutMinimum = z.string().regex(new RegExp("\\S")); + +const cweId = z + .string() + .regex(new RegExp("^\\s*[Cc][Ww][Ee]-0*[1-9][0-9]*\\s*$")); + +const locationRole = z.enum(discoveryLocationRoles); + +const rawDiscoveryLocation = z.strictObject({ + path: nonEmptyTextWithoutMinimum, + start_line: integer(1), + end_line: integer(1).optional(), + role: locationRole, +}); + +const normalizedDiscoveryLocation = rawDiscoveryLocation.extend({ + end_line: integer(1), +}); + +const rawDiscoveryCandidate = z.strictObject({ + cwe_ids: z.array(cweId), + locations: z.array(rawDiscoveryLocation).min(1), + summary: nonEmptyTextWithoutMinimum, + evidence: nonEmptyTextWithoutMinimum, + context: nonEmptyTextWithoutMinimum.optional(), + instance: nonEmptyTextWithoutMinimum.optional(), +}); + +const discoveryCandidate = z.looseObject({ + candidate_id: z.string().regex(new RegExp("^(?!\\.{1,2}$)[^/\\\\]+$")), + ...rawDiscoveryCandidate.shape, + cwe_ids: z.array(z.string().regex(new RegExp("^CWE-[1-9][0-9]*$"))), + locations: z.array(normalizedDiscoveryLocation).min(1), +}); + +const recordDiscoveryCandidatesInput = z.strictObject({ + candidates: z.array(rawDiscoveryCandidate), +}); + +const attackPath_attackPathFields = { + dataflow: nonEmptyText, + reachability: nonEmptyText, + counterevidence: nonEmptyText, + impact: z.enum(["high", "medium", "low", "ignore", "unknown"]), + likelihood: z.enum(["high", "medium", "low", "ignore", "unknown"]), + severity_rationale: nonEmptyText, + change_conditions: nonEmptyText, +}; +const { severity_rationale, change_conditions, ...attackPathEvidence } = + attackPath_attackPathFields; + +const attackPath_reportableAttackPath = z.looseObject({ + decision: z.literal("reportable"), + ...attackPathEvidence, + severity: z.enum(["critical", "high", "medium", "low"]), + severity_rationale, + change_conditions, +}); +const attackPath_ignoredAttackPath = attackPath_reportableAttackPath.extend({ + decision: z.literal("ignore"), + severity: z.literal("ignore"), +}); +const attackPath_deferredAttackPath = attackPath_reportableAttackPath.extend({ + decision: z.literal("deferred"), + severity: z.enum(["critical", "high", "medium", "low", "unknown"]), + proof_gap: nonEmptyText, +}); + +const attackPath_attackPath = z.xor([ + attackPath_reportableAttackPath, + attackPath_ignoredAttackPath, + attackPath_deferredAttackPath, +]); + +const attackPath_attackPathUpdate = z.strictObject({ + candidateId: candidateId, + attackPath: attackPath_attackPath, +}); + +const validation_validationRecord = z.looseObject({ + disposition: z + .enum(["reportable", "suppressed", "not_applicable", "deferred"]) + .register(artifactJsonMetadata, { type: "string" }), + method: nonEmptyText, + confidence: z + .enum(["high", "medium", "low"]) + .register(artifactJsonMetadata, { type: "string" }), + confidence_rationale: nonEmptyText, + rubric: z.union([ + nonEmptyText, + z.looseObject({}), + z.array(z.union([nonEmptyText, z.looseObject({})])).min(1), + ]), + evidence: z.union([nonEmptyText, z.array(nonEmptyText).min(1)]), + counterevidence_or_proof_gap: z.string(), + remaining_uncertainty: z.string(), + artifact_paths: z.array(nonEmptyText).optional(), + source: nonEmptyText.optional(), + control: nonEmptyText.optional(), + sink: nonEmptyText.optional(), + preconditions: z + .union([nonEmptyText, z.array(nonEmptyText).min(1)]) + .optional(), +}); + +const validation_validationUpdate = z.strictObject({ + candidateId: candidateId, + validation: validation_validationRecord, +}); + +const prepareInput = z.strictObject({ + scanId: scanId, + handoffClaimToken: nonEmptyText.optional(), +}); + +const reviewItem = z.strictObject({ + path: repositoryPath, +}); + +const { + textList: draft_textList, + slug, + identity: draft_identity, + severity: draft_severity, + confidence: draft_confidence, + location: draft_location, + codeEvidence, + legacyCodeEvidence: draft_legacyCodeEvidence, + findingAssessment: draft_findingAssessment, + textOrSummary: draft_textOrSummary, + findingReachability: draft_findingReachability, + findingValidation: draft_findingValidation, + findingAttackPath: draft_findingAttackPath, +} = findingDetailSchemas(nonEmptyText, repositoryPath, integer(1)); + +const draft_handoffClaimToken = z + .string() + .regex( + new RegExp( + "^(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|recovery_[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$", + ), + ); + +const draft_taxonomy = z.looseObject({ + category: nonEmptyText.describe( + "The actual primary broken security control, not a CWE identifier.", + ), + cwe: draft_textList.describe( + "Copy the reviewed candidate's exact cwe_ids array. Use an empty array when no CWE is established; do not invent one.", + ), +}); + +const draft_codeEvidence = codeEvidence.extend({ + code: nonEmptyText.describe( + "The genuine, nonempty source snippet at this evidence location.", + ), +}); + +// Zod applies uniqueItems to raw input before parsing each item. +const uniqueArray = z.fromJSONSchema({ type: "array", uniqueItems: true }); + +const draft_finding = z.looseObject({ + findingId: z.never().optional(), + occurrenceId: z.never().optional(), + fingerprints: z.never().optional(), + ruleId: slug.describe( + "A stable lowercase vulnerability-family slug, such as prototype-pollution.json-patch; a CWE is taxonomy, not a rule ID.", + ), + identity: draft_identity.optional(), + title: nonEmptyText, + summary: nonEmptyText, + severity: draft_severity, + confidence: draft_confidence, + taxonomy: draft_taxonomy, + locations: z.array(draft_location).min(1), + writeup: z + .looseObject({ + reportPath: pattern( + z.string(), + "^findings/([a-z0-9][a-z0-9._-]*)/\\1\\.md$", + ), + }) + .optional(), + codeEvidence: z.array(draft_codeEvidence).optional(), + code_evidence: z.array(draft_legacyCodeEvidence).optional(), + rootCause: z + .union([ + nonEmptyText, + z.looseObject({ + summary: nonEmptyText, + code: nonEmptyText.optional(), + language: nonEmptyText.optional(), + evidenceRefs: draft_textList.optional(), + }), + ]) + .optional(), + root_cause: z + .union([ + nonEmptyText, + z.looseObject({ + summary: nonEmptyText.optional(), + code: nonEmptyText.optional(), + language: nonEmptyText.optional(), + evidenceRefs: draft_textList.optional(), + evidence_refs: draft_textList.optional(), + }), + ]) + .optional(), + remediation: nonEmptyText, + validation: z.union([draft_findingValidation, z.null()]).optional(), + attackPath: z.union([draft_findingAttackPath, z.null()]).optional(), + remediationTests: draft_textList.optional(), + preventiveControls: draft_textList.optional(), + provenance: z.looseObject({ + sourceFindingIds: z + .transform((value, context) => { + const result = uniqueArray.safeParse(value); + if (!result.success) + for (const issue of result.error.issues) + context.addIssue({ ...issue }); + return value; + }) + .pipe( + z + .array(nonEmptyText) + .meta({ uniqueItems: true }) + .describe( + "Host-provided source finding references represented by this Deep reduction. Preserve every assigned reference exactly once.", + ), + ) + .optional(), + sourceFindings: z + .array(z.strictObject({ id: nonEmptyText, finding: z.looseObject({}) })) + .describe( + "Original source payloads retained by the host for lossless semantic merges.", + ) + .optional(), + source: nonEmptyText.describe( + "The actual finding producer. Use local_plugin only for a finding discovered by this plugin.", + ), + }), + extensions: z.looseObject({}).optional(), +}); + +const draft_scope = z.looseObject({ + includePaths: z.never().optional(), + excludePaths: z.never().optional(), + summary: nonEmptyText.optional(), + artifactsReviewed: draft_textList.optional(), + runtimeStatus: nonEmptyText.optional(), + validationMode: nonEmptyText.optional(), + context: nonEmptyText.optional(), + limitations: draft_textList.optional(), +}); + +const { schema: draft_threatModel } = threatModelSchemas(draft_textList); + +export const surfaceDispositionSchema = z.enum([ + "reported", + "no_issue_found", + "rejected", + "not_applicable", + "needs_follow_up", +]); + +const draft_surface = z.looseObject({ + id: z.string().regex(new RegExp("^[a-z0-9][a-z0-9._/-]*$")).optional(), + label: nonEmptyText.describe( + "The meaningful name of the reviewed security surface.", + ), + disposition: surfaceDispositionSchema.describe( + "The evidence-supported review result for this surface.", + ), + receiptRefs: z + .array( + pattern( + z.string().min(1), + "^(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\)artifacts/.+$", + ), + ) + .optional(), + riskArea: nonEmptyText.optional(), + notes: nonEmptyText.optional(), +}); + +const draft_coverage = z.looseObject({ + documentType: z.never().optional(), + schemaVersion: z.never().optional(), + scanId: z.never().optional(), + mode: z.never().optional(), + includePaths: z.never().optional(), + excludePaths: z.never().optional(), + receiptRefs: z.never().optional(), + completeness: z + .enum(["complete", "partial", "unknown"]) + .describe( + "Use partial if any work is deferred or any surface needs follow-up; use complete only when no such work remains.", + ), + inventoryStrategy: z.never().optional(), + surfaces: z.array(draft_surface), + explicitExclusions: z.array( + z.looseObject({ pattern: nonEmptyText, reason: nonEmptyText }), + ), + deferred: z + .array( + z.looseObject({ + id: nonEmptyText.optional(), + candidateId: candidateId.optional(), + reason: nonEmptyText, + paths: draft_textList.optional(), + surfaceIds: draft_textList.optional(), + }), + ) + .describe( + "Each supplied id identifies one task and must be unique within this draft. Reuse saved IDs in later updates.", + ), + resolvedDeferred: z + .array(z.strictObject({ id: nonEmptyText, reason: nonEmptyText })) + .describe( + "On final Standard or diff drafts, close generic tasks by saved ID and completion reason. Reuse saved surface IDs for updates. Candidate tasks need findings or dispositions; redundant closure entries for those outcomes are ignored.", + ) + .optional(), + openQuestions: z + .array( + z.union([ + nonEmptyText, + z.looseObject({ + question: nonEmptyText, + followUpPrompt: nonEmptyText.optional(), + }), + ]), + ) + .optional(), +}); + +const draft_scanDraftInput = z.strictObject({ + complete: z + .boolean() + .describe( + "Set false to save progress without declaring this worker or parent audit finished. Omit or set true only for the terminal result. Put provisional candidates in coverage.deferred, not findings.", + ) + .optional(), + scanId: scanId, + handoffClaimToken: draft_handoffClaimToken.optional(), + scope: draft_scope.optional(), + threatModel: draft_threatModel.optional(), + findings: z.array(draft_finding), + coverage: draft_coverage, +}); + +/** Native tool contracts; JSON resources are generated compatibility exports. */ +export const artifactSchemaDocuments = { + "definitions/artifact-common": { + $defs: { + nonEmptyText: nonEmptyText, + scanId: scanId, + repositoryPath: repositoryPath, + candidateId: candidateId, + }, + }, + "definitions/discovery-candidate": { + $defs: { + nonEmptyText: nonEmptyTextWithoutMinimum, + cweId: cweId, + locationRole: locationRole, + rawDiscoveryLocation: rawDiscoveryLocation, + normalizedDiscoveryLocation: normalizedDiscoveryLocation, + rawDiscoveryCandidate: rawDiscoveryCandidate, + discoveryCandidate: discoveryCandidate, + }, + }, + "tools/discovery-candidates": { + $defs: { + workbenchRecordDiscoveryCandidatesInput: z.strictObject({ + scanId: nonEmptyTextWithoutMinimum, + ...recordDiscoveryCandidatesInput.shape, + }), + workbenchListCandidatesInput: z.strictObject({ + scanId: nonEmptyTextWithoutMinimum, + ...page.shape, + }), + }, + }, + "tools/candidate-attack-paths": { + title: "Record Codex Security candidate attack paths", + $ref: "#/$defs/input", + $defs: { + input: z.strictObject({ + scanId: scanId, + attackPaths: z.array(attackPath_attackPathUpdate), + }), + attackPathUpdate: attackPath_attackPathUpdate, + attackPath: attackPath_attackPath, + attackPathFields: attackPath_attackPathFields, + reportableAttackPath: attackPath_reportableAttackPath, + ignoredAttackPath: attackPath_ignoredAttackPath, + deferredAttackPath: attackPath_deferredAttackPath, + }, + }, + "tools/candidate-validations": { + title: "Record compact Codex Security candidate validations", + $ref: "#/$defs/input", + $defs: { + input: z.strictObject({ + scanId: scanId, + validations: z.array(validation_validationUpdate), + }), + validationUpdate: validation_validationUpdate, + validationRecord: validation_validationRecord, + }, + }, + "tools/review-items": { + $defs: { + prepareInput: prepareInput, + reviewItemsInput: prepareInput.extend(page.shape), + reviewItem: reviewItem, + }, + }, + "tools/scan-draft": { + title: "Codex Security semantic scan draft", + $ref: "#/$defs/scanDraftInput", + $defs: { + text: nonEmptyText, + textList: draft_textList, + scanId: scanId, + handoffClaimToken: draft_handoffClaimToken, + repositoryPath: repositoryPath, + location: draft_location, + identity: draft_identity, + severity: draft_severity, + confidence: draft_confidence, + taxonomy: draft_taxonomy, + codeEvidence: draft_codeEvidence, + legacyCodeEvidence: draft_legacyCodeEvidence, + findingAssessment: draft_findingAssessment, + textOrSummary: draft_textOrSummary, + findingReachability: draft_findingReachability, + findingValidation: draft_findingValidation, + findingAttackPath: draft_findingAttackPath, + finding: draft_finding, + scope: draft_scope, + threatModel: draft_threatModel, + surface: draft_surface, + coverage: draft_coverage, + scanDraftInput: draft_scanDraftInput, + completedScanInput: z.strictObject({ + scanId: scanId, + handoffClaimToken: draft_handoffClaimToken.optional(), + }), + }, + }, + "tools/deep-reducer": { + title: "Codex Security Deep Scan semantic reducer operations", + $ref: "#/$defs/reductionInput", + $defs: { + reducerInputs: z.strictObject({ + cursor: z + .string() + .regex(new RegExp("^(0|[1-9][0-9]*)$")) + .describe("Continue the same document from its returned nextCursor.") + .optional(), + maxBytes: integer(1, 9007199254740991).describe( + "Caller-selected byte budget for the serialized MCP response. Choose below the transport and output budgets; reduce it if transport reports an oversized response.", + ), + findingRef: z + .string() + .describe( + "Omit to page assigned findings and the previous aggregate without retained provenance bodies. Use source: to fetch a complete original, or previous: to fetch a complete previous aggregate finding, including its retained history.", + ) + .optional(), + }), + reductionInput: draft_scanDraftInput.omit({ coverage: true }).extend({ + complete: z + .boolean() + .describe("Omit or set true when submitting the finished aggregate.") + .optional(), + }), + }, + }, +}; diff --git a/plugins/codex-security/mcp-app/src/artifact-deep-reducer-pages.ts b/plugins/codex-security/mcp-app/src/artifact-deep-reducer-pages.ts index 66230d59b5..2ae39ddceb 100644 --- a/plugins/codex-security/mcp-app/src/artifact-deep-reducer-pages.ts +++ b/plugins/codex-security/mcp-app/src/artifact-deep-reducer-pages.ts @@ -1,3 +1,4 @@ +import type * as z from "zod/v4"; import type { ArtifactContext } from "./artifact-context.js"; import { deepReducerInputsInputSchema, @@ -8,11 +9,7 @@ import type { DeepReductionSources, } from "./deep-scan/artifact-validation.js"; -export interface DeepReducerPageInput { - cursor?: string; - maxBytes: number; - findingRef?: string; -} +export type DeepReducerPageInput = z.infer; /** Concatenate every json fragment for one document before parsing it. */ export interface DeepReducerPage { @@ -111,7 +108,7 @@ function projectFinding(finding: Finding, refs?: string[]): Finding { previousFindings: _previous, originalCandidates: _candidates, ...provenance - } = finding.provenance as Record; + } = finding.provenance; return { ...finding, provenance: { diff --git a/plugins/codex-security/mcp-app/src/artifact-deep-reducer.ts b/plugins/codex-security/mcp-app/src/artifact-deep-reducer.ts index 5c30ae8597..712a121d82 100644 --- a/plugins/codex-security/mcp-app/src/artifact-deep-reducer.ts +++ b/plugins/codex-security/mcp-app/src/artifact-deep-reducer.ts @@ -1,18 +1,10 @@ import { join } from "node:path"; -import type { ZodType } from "zod/v4"; -import commonSchema from "../../schemas/definitions/artifact-common.schema.json"; -import reducerSchema from "../../schemas/tools/deep-reducer.schema.json"; -import scanDraftSchema from "../../schemas/tools/scan-draft.schema.json"; import type { ArtifactContext } from "./artifact-context.js"; -import type { DeepReducerPageInput } from "./artifact-deep-reducer-pages.js"; import { parsePersistedScanDraft, saveScanDraftCheckpoint, } from "./artifact-scan-draft.js"; -import { - loadArtifactZodSchema, - type SchemaDocument, -} from "./artifact-schema-loader.js"; +import { artifactSchemaDocuments } from "./artifact-contracts.js"; import { saveThreatModelDocument } from "./threat-model-document.js"; import { createDeepScanArtifacts, @@ -28,23 +20,10 @@ import { type DeepReductionSources, } from "./deep-scan/artifact-validation.js"; -const schemaDocuments = [ - commonSchema, - scanDraftSchema, - reducerSchema, -] as SchemaDocument[]; - -export const deepReducerInputsInputSchema = loadArtifactZodSchema( - schemaDocuments, - reducerSchema.$id, - "reducerInputs", -) as ZodType; - -export const deepReductionInputSchema = loadArtifactZodSchema( - schemaDocuments, - reducerSchema.$id, - "reductionInput", -) as ZodType; +export const { + reducerInputs: deepReducerInputsInputSchema, + reductionInput: deepReductionInputSchema, +} = artifactSchemaDocuments["tools/deep-reducer"].$defs; /** Read the findings and scan context assigned to this reducer. */ export async function getCodexSecurityDeepReducerInputs( diff --git a/plugins/codex-security/mcp-app/src/artifact-discovery.ts b/plugins/codex-security/mcp-app/src/artifact-discovery.ts index d04a996684..552245a64d 100644 --- a/plugins/codex-security/mcp-app/src/artifact-discovery.ts +++ b/plugins/codex-security/mcp-app/src/artifact-discovery.ts @@ -12,8 +12,6 @@ import { type normalizeCandidateBatch, } from "./helpers/normalize-candidates.js"; import type * as z from "zod/v4"; -import discoveryCandidateDefinitions from "../../schemas/definitions/discovery-candidate.schema.json"; -import discoveryCandidatesToolSchema from "../../schemas/tools/discovery-candidates.schema.json"; import type { ArtifactContext } from "./artifact-context.js"; import { artifactDestination, @@ -23,70 +21,33 @@ import { artifactSourcePath, replaceArtifactText, } from "./artifact-io.js"; -import { - loadArtifactZodSchema, - type SchemaDocument, -} from "./artifact-schema-loader.js"; +import { artifactSchemaDocuments } from "./artifact-contracts.js"; import { candidateSchemaV1 } from "./deep-scan/artifact-contracts.js"; const discoveryComponents = ["artifacts", "02_discovery"] as const; const discoveryLabel = "discovery candidates"; -const discoverySchemaDocuments = [ - discoveryCandidateDefinitions, - discoveryCandidatesToolSchema, -] as SchemaDocument[]; - -export type RawDiscoveryLocationRole = - | "entrypoint" - | "entrypoint/wrapper" - | "source" - | "root_control" - | "sink" - | "concrete_implementation" - | "evidence"; - -export interface RawDiscoveryLocation { - path: string; - start_line: number; - end_line?: number; - role: RawDiscoveryLocationRole; -} - -export interface RawDiscoveryCandidate { - cwe_ids: string[]; - locations: RawDiscoveryLocation[]; - summary: string; - evidence: string; - context?: string; - instance?: string; -} - -export interface DiscoveryCandidatesInput { - candidates: RawDiscoveryCandidate[]; -} - -export type CompactDiscoveryCandidate = z.infer & - Record; - -/** Every exposed validator is derived from the checked-in JSON Schema source. */ -export const compactDiscoveryCandidateSchema = loadArtifactZodSchema( - discoverySchemaDocuments, - discoveryCandidateDefinitions.$id, - "discoveryCandidate", -) as z.ZodType; - -export const workbenchDiscoveryCandidatesInputSchema = loadArtifactZodSchema( - discoverySchemaDocuments, - discoveryCandidatesToolSchema.$id, - "workbenchRecordDiscoveryCandidatesInput", -) as z.ZodType; - -export const workbenchListCodexSecurityCandidatesInputSchema = - loadArtifactZodSchema( - discoverySchemaDocuments, - discoveryCandidatesToolSchema.$id, - "workbenchListCandidatesInput", - ) as z.ZodType; +const definitions = + artifactSchemaDocuments["definitions/discovery-candidate"].$defs; +export type RawDiscoveryLocationRole = z.infer; +export type RawDiscoveryLocation = z.infer< + typeof definitions.rawDiscoveryLocation +>; +export type RawDiscoveryCandidate = z.infer< + typeof definitions.rawDiscoveryCandidate +>; +export const compactDiscoveryCandidateSchema = definitions.discoveryCandidate; +export type CompactDiscoveryCandidate = z.infer< + typeof compactDiscoveryCandidateSchema +>; +export const { + workbenchRecordDiscoveryCandidatesInput: + workbenchDiscoveryCandidatesInputSchema, + workbenchListCandidatesInput: workbenchListCodexSecurityCandidatesInputSchema, +} = artifactSchemaDocuments["tools/discovery-candidates"].$defs; +export type DiscoveryCandidatesInput = Pick< + z.infer, + "candidates" +>; /** * Normalize in memory and replace the bound canonical candidate ledger. diff --git a/plugins/codex-security/mcp-app/src/artifact-inventory.ts b/plugins/codex-security/mcp-app/src/artifact-inventory.ts index 4852f14919..14908c7845 100644 --- a/plugins/codex-security/mcp-app/src/artifact-inventory.ts +++ b/plugins/codex-security/mcp-app/src/artifact-inventory.ts @@ -3,8 +3,6 @@ import { readFile } from "node:fs/promises"; import { join } from "node:path"; import { promisify } from "node:util"; import * as z from "zod/v4"; -import commonSchema from "../../schemas/definitions/artifact-common.schema.json"; -import reviewItemsSchema from "../../schemas/tools/review-items.schema.json"; import { artifactDestination, artifactSourcePath, @@ -12,10 +10,7 @@ import { type ArtifactContext, type ArtifactPage, } from "./artifact-io.js"; -import { - loadArtifactZodSchema, - type SchemaDocument, -} from "./artifact-schema-loader.js"; +import { artifactSchemaDocuments } from "./artifact-contracts.js"; import { missingPythonHelperMessage, resolvePythonCommand, @@ -23,31 +18,16 @@ import { import { decodeUtf8 } from "./helpers/utf8.js"; const execFile = promisify(nodeExecFile); -const documents = [commonSchema, reviewItemsSchema] as SchemaDocument[]; const inventoryComponents = ["artifacts", "02_discovery", "in_scope_files.txt"]; const label = "review_items"; -export interface ReviewItem { - path: string; -} - -export const prepareReviewItemsInputSchema = loadArtifactZodSchema( - documents, - reviewItemsSchema.$id, - "prepareInput", -) as z.ZodType<{ scanId: string; handoffClaimToken?: string }>; - -export const reviewItemsReaderInputSchema = loadArtifactZodSchema( - documents, - reviewItemsSchema.$id, - "reviewItemsInput", -) as z.ZodType<{ scanId: string; handoffClaimToken?: string } & ArtifactPage>; - -const reviewItemSchema = loadArtifactZodSchema( - documents, - reviewItemsSchema.$id, - "reviewItem", -) as z.ZodType; +const definitions = artifactSchemaDocuments["tools/review-items"].$defs; +export type ReviewItem = z.infer; +export const { + prepareInput: prepareReviewItemsInputSchema, + reviewItemsInput: reviewItemsReaderInputSchema, + reviewItem: reviewItemSchema, +} = definitions; /** Build the selected repository or diff inventory from host-bound scan context. */ export async function prepareCodexSecurityReviewItems( diff --git a/plugins/codex-security/mcp-app/src/artifact-scan-draft.ts b/plugins/codex-security/mcp-app/src/artifact-scan-draft.ts index 01d011a04d..4cde37d5c3 100644 --- a/plugins/codex-security/mcp-app/src/artifact-scan-draft.ts +++ b/plugins/codex-security/mcp-app/src/artifact-scan-draft.ts @@ -5,8 +5,6 @@ import { promises as fs } from "node:fs"; import { dirname, join, sep } from "node:path"; import { isDeepStrictEqual } from "node:util"; import type * as z from "zod/v4"; -import commonSchema from "../../schemas/definitions/artifact-common.schema.json"; -import scanDraftDocument from "../../schemas/tools/scan-draft.schema.json"; import type { ArtifactContext } from "./artifact-context.js"; import type { RunArtifactWorkbench } from "./artifact-context.js"; import { @@ -16,26 +14,16 @@ import { readArtifactTextWithMetadata, replaceArtifactJson, } from "./artifact-io.js"; -import { - loadArtifactZodSchema, - type SchemaDocument, -} from "./artifact-schema-loader.js"; +import { artifactSchemaDocuments } from "./artifact-contracts.js"; import { saveThreatModelDocument } from "./threat-model-document.js"; -export interface ScanDraftInput { - scanId: string; - complete?: boolean; - handoffClaimToken?: string; - scope?: JsonObject; - threatModel?: JsonObject; - findings: JsonObject[]; - coverage: JsonObject; -} - -export interface CompletedScanInput { - scanId: string; - handoffClaimToken?: string; -} +export const { + scanDraftInput: scanDraftInputSchema, + completedScanInput: completedScanInputSchema, +} = artifactSchemaDocuments["tools/scan-draft"].$defs; +export type ScanDraftInput = z.infer; +type DraftCoverage = ScanDraftInput["coverage"]; +export type CompletedScanInput = z.infer; export interface ScanDraftResult { scanId: string; @@ -64,24 +52,10 @@ interface SavedScanDraft { attempt?: string; } -const schemaDocuments = [commonSchema, scanDraftDocument] as SchemaDocument[]; - -export const scanDraftInputSchema = loadArtifactZodSchema( - schemaDocuments, - scanDraftDocument.$id, - "scanDraftInput", -) as z.ZodType; - -export const completedScanInputSchema = loadArtifactZodSchema( - schemaDocuments, - scanDraftDocument.$id, - "completedScanInput", -) as z.ZodType; - /** Stage a parent draft, then publish it under the workbench completion lock. */ export async function recordCodexSecurityScanDraftViaWorkbench( context: ArtifactContext, - input: ScanDraftInput, + input: unknown, runWorkbench: RunArtifactWorkbench, signal?: AbortSignal, ): Promise { @@ -162,7 +136,7 @@ export async function recordCodexSecurityScanDraftViaWorkbench( const arguments_ = [ "write-scan-draft", "--scan-id", - input.scanId, + checkpoint.scanId, "--draft-path", draftPath, "--checkpoint-path", @@ -207,7 +181,7 @@ export async function recordCodexSecurityScanDraftViaWorkbench( /** Save a Standard worker draft in its assigned output directory. */ export async function recordCodexSecurityWorkerScanDraft( context: ArtifactContext, - input: ScanDraftInput, + input: unknown, ): Promise { const parsed = parseScanDraft(input); if (context.layout !== "worker") { @@ -327,19 +301,15 @@ async function preserveScanDraft( const savedSources = [...current, ...archived]; const sources = savedSources.map(({ input }) => input); // Older checkpoints can omit IDs already assigned in their published output. - const savedDeferred = sources.flatMap( - (source) => source.coverage.deferred as JsonObject[], - ); - const savedSurfaces = sources.flatMap( - (source) => source.coverage.surfaces as JsonObject[], - ); + const savedDeferred = sources.flatMap((source) => source.coverage.deferred); + const savedSurfaces = sources.flatMap((source) => source.coverage.surfaces); for (const source of sources) { const reservedSurfaceIds = new Set( - (source.coverage.surfaces as JsonObject[]).flatMap((row) => + source.coverage.surfaces.flatMap((row) => typeof row.id === "string" ? [row.id] : [], ), ); - const surfaces = (source.coverage.surfaces as JsonObject[]).map((row) => { + const surfaces = source.coverage.surfaces.map((row) => { if (typeof row.id === "string") return row; const matching = savedSurfaces.find( ({ id, ...content }) => @@ -361,12 +331,12 @@ async function preserveScanDraft( typeof row.id === "string" ? row : normalizedSurfaces[index]!, ); const reservedIds = new Set( - (source.coverage.deferred as JsonObject[]).flatMap((row) => + source.coverage.deferred.flatMap((row) => typeof row.id === "string" ? [row.id] : [], ), ); source.coverage.deferred = normalizeDeferred( - (source.coverage.deferred as JsonObject[]).map((row) => { + source.coverage.deferred.map((row) => { if ( typeof row.id === "string" || "candidateId" in row || @@ -411,7 +381,7 @@ async function preserveScanDraft( const reopenedSurfaces = new Set(); if (retainedFinal) { const terminalOutcomeIds = completedCandidateIds(retainedFinal.input); - for (const surface of result.coverage.surfaces as JsonObject[]) { + for (const surface of result.coverage.surfaces) { if ( terminalOutcomeIds.has((surface.candidateId ?? surface.id) as string) && typeof surface.id === "string" @@ -437,7 +407,7 @@ async function preserveScanDraft( for (const observation of progressSources) { const progress = structuredClone(observation); const reopenedIds = new Set( - (progress.coverage.deferred as JsonObject[]).flatMap((row) => + progress.coverage.deferred.flatMap((row) => [row.id, row.candidateId].filter( (id): id is string => typeof id === "string" && closedIds.has(id), ), @@ -458,12 +428,12 @@ async function preserveScanDraft( result.coverage = preserveScanCoverage( { ...result.coverage, - deferred: (progress.coverage.deferred as JsonObject[]).filter( + deferred: progress.coverage.deferred.filter( (row) => keepsGenericWork(row) || !terminalOutcomeIds.has((row.candidateId ?? row.id) as string), ), - surfaces: (progress.coverage.surfaces as JsonObject[]).filter( + surfaces: progress.coverage.surfaces.filter( (surface) => keepsGenericWork(surface) || (!terminalOutcomeIds.has(surface.id as string) && @@ -492,7 +462,7 @@ async function preserveScanDraft( await saveScanDraftCheckpoint(context, input, false); const resolvedFollowUpSurfaces = sources.flatMap((source) => { - const pending = source.coverage.deferred as JsonObject[]; + const pending = source.coverage.deferred; if ( pending.length === 0 || pending.some((item) => { @@ -505,21 +475,21 @@ async function preserveScanDraft( }) ) return []; - return (source.coverage.surfaces as JsonObject[]).filter( + return source.coverage.surfaces.filter( (surface) => surface.disposition === "needs_follow_up", ); }); for (const source of sources) { - const deferred = result.coverage.deferred as JsonObject[]; - const dispositions = (result.coverage.surfaces as JsonObject[]).filter( + const deferred = result.coverage.deferred; + const dispositions = result.coverage.surfaces.filter( (surface) => (surface.disposition === "rejected" || surface.disposition === "not_applicable") && typeof surface.candidateId === "string", ); const candidateRows = [...deferred, ...dispositions]; - for (const pending of source.coverage.deferred as JsonObject[]) { + for (const pending of source.coverage.deferred) { const candidateId = pending.candidateId ?? pending.id; if (typeof candidateId !== "string") continue; const finding = result.findings.find( @@ -587,7 +557,7 @@ async function preserveScanDraft( ); const previousCoverage = { ...source.coverage, - deferred: (source.coverage.deferred as JsonObject[]).filter((item) => { + deferred: source.coverage.deferred.filter((item) => { const candidateId = item.candidateId ?? item.id; return ( (keepsGenericWork(item) || @@ -596,7 +566,7 @@ async function preserveScanDraft( !closedDeferredIds.has(item.id as string) ); }), - surfaces: (source.coverage.surfaces as JsonObject[]).filter((surface) => { + surfaces: source.coverage.surfaces.filter((surface) => { if (resolvedSurfaces.has(surface)) return false; const candidateId = surface.candidateId ?? surface.id; return ( @@ -616,20 +586,17 @@ async function preserveScanDraft( }), openQuestions: result.complete === false - ? ( - (source.coverage.openQuestions as unknown[] | undefined) ?? [] - ).filter( + ? (source.coverage.openQuestions ?? []).filter( (question) => !coverageEntryPresent( - (result.coverage.openQuestions as unknown[] | undefined) ?? - [], + result.coverage.openQuestions ?? [], question, ), ) : [], }; result.coverage.surfaces = exactUnion( - result.coverage.surfaces as JsonObject[], + result.coverage.surfaces, previousCoverage.surfaces, ); result.coverage = preserveScanCoverage( @@ -638,12 +605,8 @@ async function preserveScanDraft( ambiguousDeferredIds, ); } - result.coverage.deferred = normalizeDeferred( - result.coverage.deferred as JsonObject[], - ); - result.coverage.surfaces = normalizeSurfaces( - result.coverage.surfaces as JsonObject[], - ); + result.coverage.deferred = normalizeDeferred(result.coverage.deferred); + result.coverage.surfaces = normalizeSurfaces(result.coverage.surfaces); if (saveCheckpoint) await saveScanDraftCheckpoint(context, result); return { input: result, previousDigest: previousState.digest }; } @@ -659,7 +622,7 @@ function completedCandidateIds( const id = findingCandidateId(finding); if (id !== undefined) completed.add(id); } - const dispositions = (source.coverage.surfaces as JsonObject[]).filter( + const dispositions = source.coverage.surfaces.filter( (surface) => surface.disposition === "rejected" || surface.disposition === "not_applicable", @@ -668,10 +631,7 @@ function completedCandidateIds( const id = surface.candidateId; if (typeof id === "string" && !observed.has(id)) completed.add(id); } - for (const row of [ - ...(source.coverage.deferred as JsonObject[]), - ...dispositions, - ]) { + for (const row of [...source.coverage.deferred, ...dispositions]) { const id = row.candidateId ?? row.id; if (typeof id === "string") observed.add(id); } @@ -679,8 +639,8 @@ function completedCandidateIds( return completed; } -function resolvedDeferred(coverage: JsonObject): JsonObject[] { - return (coverage.resolvedDeferred as JsonObject[] | undefined) ?? []; +function resolvedDeferred(coverage: DraftCoverage) { + return coverage.resolvedDeferred ?? []; } function genericDeferred(row: JsonObject): boolean { @@ -703,7 +663,7 @@ function ambiguousGenericEntry( function ambiguousGenericDeferredIds(sources: ScanDraftInput[]): Set { const ambiguous = new Set(); for (const source of sources) { - const rows = source.coverage.deferred as JsonObject[]; + const rows = source.coverage.deferred; const candidateIds = new Set( rows .filter((row) => !genericDeferred(row)) @@ -730,18 +690,18 @@ function ambiguousGenericDeferredIds(sources: ScanDraftInput[]): Set { function reconcileResolvedDeferred( result: ScanDraftInput, - requestedClosures: JsonObject[], + requestedClosures: NonNullable, sources: ScanDraftInput[], savedSources: SavedScanDraft[], resolvedCandidateIds: Set, ambiguousIds: Set, retainedFinal?: ScanDraftInput, ): { closedDeferredIds: Set; resolvedSurfaces: Set } { - const activeDeferred = result.coverage.deferred as JsonObject[]; + const activeDeferred = result.coverage.deferred; // Legacy aliases cannot identify which independent task was completed. // Keep generic work separate from candidate outcomes and ambiguous closures. for (const source of sources) { - for (const row of source.coverage.deferred as JsonObject[]) { + for (const row of source.coverage.deferred) { if ( ambiguousGenericEntry(row, ambiguousIds) && !activeDeferred.some((current) => isDeepStrictEqual(current, row)) @@ -762,16 +722,14 @@ function reconcileResolvedDeferred( for (const source of savedSources) { const key = JSON.stringify([source.attempt, source.modifiedMs]); const pending = pendingByTime.get(key) ?? new Set(); - for (const row of source.input.coverage.deferred as JsonObject[]) { + for (const row of source.input.coverage.deferred) { pending.add(row.id as string); if (typeof row.candidateId === "string") pending.add(row.candidateId); } pendingByTime.set(key, pending); tiedPending.set(source.input, pending); } - const historical = sources.flatMap( - (source) => source.coverage.deferred as JsonObject[], - ); + const historical = sources.flatMap((source) => source.coverage.deferred); const candidateRows = historical.filter( (row) => typeof row.candidateId === "string" || @@ -785,12 +743,15 @@ function reconcileResolvedDeferred( ), ), ); - const closures = new Map(); + const closures = new Map< + string, + NonNullable[number] + >(); const previouslyClosed = new Set(); const closureSources = new Map(); for (const source of sources) { // Keep the first saved state for each ID so reopened work stays pending. - for (const row of source.coverage.deferred as JsonObject[]) { + for (const row of source.coverage.deferred) { observedIds.add(row.id as string); if (typeof row.candidateId === "string") observedIds.add(row.candidateId); } @@ -872,7 +833,7 @@ function reconcileResolvedDeferred( ...new Set([ ...closureSources.values(), ...sources.filter((source) => - (source.coverage.deferred as JsonObject[]).some((row) => + source.coverage.deferred.some((row) => reopenedIds.has(row.id as string), ), ), @@ -908,7 +869,7 @@ function reconcileDeferredSurfaces( if (workIds.size === 0) return resolved; const current = coverage.surfaces as JsonObject[]; const inheritedSurfaces = inherited.flatMap((source) => - (source.coverage.surfaces as JsonObject[]).map((surface) => ({ + source.coverage.surfaces.map((surface) => ({ surface, source, })), @@ -920,7 +881,7 @@ function reconcileDeferredSurfaces( "candidateId" in surface || "candidate" in surface || "finding" in surface; const latestDeferred = [...(coverage.deferred as JsonObject[])]; for (const source of sources) { - for (const row of source.coverage.deferred as JsonObject[]) { + for (const row of source.coverage.deferred) { if (!deferredEntryPresent(latestDeferred, row, ambiguousDeferredIds)) latestDeferred.push(row); } @@ -948,8 +909,8 @@ function reconcileDeferredSurfaces( continue; const matches = sources.map((source) => ({ source, - surfaces: (source.coverage.surfaces as JsonObject[]).filter(sameSurface), - deferred: source.coverage.deferred as JsonObject[], + surfaces: source.coverage.surfaces.filter(sameSurface), + deferred: source.coverage.deferred, })); if ( matches.some( @@ -1583,10 +1544,10 @@ function coverageEntryIdentities(entry: JsonObject): string[] { /** Reducers cannot resolve source review by omitting its coverage records. */ function preserveScanCoverage( - coverage: JsonObject, - previous: JsonObject, + coverage: DraftCoverage, + previous: DraftCoverage, ambiguousDeferredIds = new Set(), -): JsonObject { +): DraftCoverage { const result = structuredClone(coverage); for (const field of [ "surfaces", @@ -1615,7 +1576,7 @@ function preserveScanCoverage( values.length > 0 || result[field] !== undefined ) - result[field] = values; + Object.assign(result, { [field]: values }); } if (coverageHasOutstandingWork(result)) result.completeness = "partial"; return result; @@ -1725,11 +1686,11 @@ export async function getCodexSecurityCompletedScan( return { scanId: parsed.scanId, manifest, findings, coverage }; } -export function parseScanDraft(input: ScanDraftInput): ScanDraftInput { +export function parseScanDraft(input: unknown): ScanDraftInput { const parsed = parseScanDraftDocument(input); const deferredIds = new Set(); const ambiguousIds = ambiguousGenericDeferredIds([parsed]); - for (const row of parsed.coverage.deferred as JsonObject[]) { + for (const row of parsed.coverage.deferred) { if (typeof row.id !== "string") continue; if (deferredIds.has(row.id) || ambiguousIds.has(row.id)) throw new Error(`scan draft: coverage.deferred repeats ${row.id}.`); @@ -1739,12 +1700,8 @@ export function parseScanDraft(input: ScanDraftInput): ScanDraftInput { throw new Error( "scan draft: coverage.resolvedDeferred is allowed only on a terminal draft.", ); - parsed.coverage.deferred = normalizeDeferred( - parsed.coverage.deferred as JsonObject[], - ); - parsed.coverage.surfaces = normalizeSurfaces( - parsed.coverage.surfaces as JsonObject[], - ); + parsed.coverage.deferred = normalizeDeferred(parsed.coverage.deferred); + parsed.coverage.surfaces = normalizeSurfaces(parsed.coverage.surfaces); return parsed; } @@ -2234,7 +2191,9 @@ function buildCoverage( }; } -function normalizeSurfaces(surfaces: JsonObject[]): JsonObject[] { +function normalizeSurfaces( + surfaces: DraftCoverage["surfaces"], +): DraftCoverage["surfaces"] { const reservedSurfaceIds = new Set( surfaces.flatMap((surface) => [surface.id, surface.candidateId].filter( @@ -2264,7 +2223,9 @@ function normalizeSurfaces(surfaces: JsonObject[]): JsonObject[] { }); } -function normalizeDeferred(rows: JsonObject[]): JsonObject[] { +function normalizeDeferred( + rows: DraftCoverage["deferred"], +): DraftCoverage["deferred"] { // Reserve later owned identities before deriving any earlier missing ones. const ids = new Set( rows.flatMap((item) => (typeof item.id === "string" ? [item.id] : [])), diff --git a/plugins/codex-security/mcp-app/src/artifact-schema-loader.ts b/plugins/codex-security/mcp-app/src/artifact-schema-loader.ts deleted file mode 100644 index c4ea6bd32a..0000000000 --- a/plugins/codex-security/mcp-app/src/artifact-schema-loader.ts +++ /dev/null @@ -1,135 +0,0 @@ -import * as z from "zod/v4"; - -export type ArtifactSchemaObject = Record; - -export type SchemaDocument = ArtifactSchemaObject & { - $id: string; - $defs: Record; -}; - -/** - * Bundle checked-in Draft 2020-12 definitions for the existing MCP registry. - */ -export function bundleArtifactSchema( - documents: readonly SchemaDocument[], - documentId: string, - definition: string, -): ArtifactSchemaObject { - const documentsById = new Map(); - for (const document of documents) { - if (documentsById.has(document.$id)) { - throw new Error( - "Duplicate Codex Security schema document: " + document.$id, - ); - } - documentsById.set(document.$id, document); - } - - const document = documentsById.get(documentId); - if (!document) { - throw new Error("Unknown Codex Security schema document: " + documentId); - } - const source = document.$defs[definition]; - if (!source || typeof source !== "object" || Array.isArray(source)) { - throw new Error("Unknown Codex Security schema definition: " + definition); - } - - const activeReferences = new Set(); - - function dereferenceArtifactSchema( - value: unknown, - document: SchemaDocument, - ): unknown { - if (Array.isArray(value)) { - return value.map((item) => dereferenceArtifactSchema(item, document)); - } - if (!value || typeof value !== "object") return value; - - const object = value as ArtifactSchemaObject; - if (typeof object.$ref !== "string") { - return Object.fromEntries( - Object.entries(object).map(([name, child]) => [ - name, - dereferenceArtifactSchema(child, document), - ]), - ); - } - - const reference = object.$ref; - const separator = reference.indexOf("#"); - const referencedDocumentId = - separator < 0 ? reference : reference.slice(0, separator); - const pointer = separator < 0 ? "" : reference.slice(separator + 1); - const referencedDocument = referencedDocumentId - ? documentsById.get(referencedDocumentId) - : document; - if (!referencedDocument) { - throw new Error("Unknown Codex Security schema reference: " + reference); - } - - const referenceKey = referencedDocument.$id + "#" + pointer; - if (activeReferences.has(referenceKey)) { - throw new Error("Cyclic Codex Security schema reference: " + reference); - } - activeReferences.add(referenceKey); - const resolved = dereferenceArtifactSchema( - readSchemaPointer(referencedDocument, pointer), - referencedDocument, - ); - // A recursive failure exits this bundle. Siblings use the parent path. - activeReferences.delete(referenceKey); - const siblings = Object.fromEntries( - Object.entries(object).filter(([name]) => name !== "$ref"), - ); - if (Object.keys(siblings).length === 0) return resolved; - if (!resolved || typeof resolved !== "object" || Array.isArray(resolved)) { - throw new Error( - "Codex Security schema reference cannot have sibling fields: " + - reference, - ); - } - return { - ...resolved, - ...(dereferenceArtifactSchema( - siblings, - document, - ) as ArtifactSchemaObject), - }; - } - - return dereferenceArtifactSchema(source, document) as ArtifactSchemaObject; -} - -/** - * Keep checked-in JSON Schema, tool validation, and tools/list in agreement. - */ -export function loadArtifactZodSchema( - documents: readonly SchemaDocument[], - documentId: string, - definition: string, -): z.ZodType { - return z.fromJSONSchema( - bundleArtifactSchema( - documents, - documentId, - definition, - ) as z.core.JSONSchema.JSONSchema, - ); -} - -function readSchemaPointer(document: SchemaDocument, pointer: string): unknown { - if (!pointer) return document; - if (!pointer.startsWith("/")) { - throw new Error("Invalid Codex Security schema pointer: #" + pointer); - } - - let value: unknown = document; - for (const encoded of pointer.slice(1).split("/")) { - const key = encoded.replaceAll("~1", "/").replaceAll("~0", "~"); - if (!value || typeof value !== "object" || !Object.hasOwn(value, key)) { - throw new Error("Unknown Codex Security schema pointer: #" + pointer); - } - value = (value as ArtifactSchemaObject)[key]; - } - return value; -} diff --git a/plugins/codex-security/mcp-app/src/deep-scan/artifact-contracts.ts b/plugins/codex-security/mcp-app/src/deep-scan/artifact-contracts.ts index 596e4d4f2a..ddddbd798a 100644 --- a/plugins/codex-security/mcp-app/src/deep-scan/artifact-contracts.ts +++ b/plugins/codex-security/mcp-app/src/deep-scan/artifact-contracts.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { discoveryLocationRoles } from "../discovery-location-roles.js"; const nonEmptyString = z .string() @@ -9,15 +10,7 @@ const candidateLocationSchemaV1 = z path: nonEmptyString, start_line: z.number().int().positive(), end_line: z.number().int().positive(), - role: z.enum([ - "entrypoint", - "entrypoint/wrapper", - "source", - "root_control", - "sink", - "concrete_implementation", - "evidence", - ]), + role: z.enum(discoveryLocationRoles), }) .refine((location) => location.end_line >= location.start_line, { message: "end_line must be greater than or equal to start_line", diff --git a/plugins/codex-security/mcp-app/src/deep-scan/artifact-validation.ts b/plugins/codex-security/mcp-app/src/deep-scan/artifact-validation.ts index d3bdd6ee8c..90e3fd3e04 100644 --- a/plugins/codex-security/mcp-app/src/deep-scan/artifact-validation.ts +++ b/plugins/codex-security/mcp-app/src/deep-scan/artifact-validation.ts @@ -190,7 +190,7 @@ export function reconcileDeepReduction( const sourceValues = [ ...discoveries.map((discovery) => discovery.result[field]), previous?.[field], - ].filter((value): value is Record => value !== undefined); + ].filter((value) => value !== undefined); if ( sourceValues.some((value) => !isDeepStrictEqual(sourceValues[0], value)) ) { @@ -199,7 +199,7 @@ export function reconcileDeepReduction( ); } if (sourceValues[0] !== undefined) { - result[field] = structuredClone(sourceValues[0]); + Object.assign(result, { [field]: structuredClone(sourceValues[0]) }); } } return result; diff --git a/plugins/codex-security/mcp-app/src/deep-scan/coordinator.ts b/plugins/codex-security/mcp-app/src/deep-scan/coordinator.ts index 9ff4b924be..08a9436d63 100644 --- a/plugins/codex-security/mcp-app/src/deep-scan/coordinator.ts +++ b/plugins/codex-security/mcp-app/src/deep-scan/coordinator.ts @@ -281,7 +281,7 @@ export class DeepScanCoordinator { // Readers require coverage.json. The coordinator has accepted this // result, so mark it complete and leave review notes empty. coverage: { - completeness: "complete", + completeness: "complete" as const, surfaces: [], explicitExclusions: [], deferred: [], diff --git a/plugins/codex-security/mcp-app/src/discovery-location-roles.ts b/plugins/codex-security/mcp-app/src/discovery-location-roles.ts new file mode 100644 index 0000000000..5aff90d572 --- /dev/null +++ b/plugins/codex-security/mcp-app/src/discovery-location-roles.ts @@ -0,0 +1,9 @@ +export const discoveryLocationRoles = [ + "entrypoint", + "entrypoint/wrapper", + "source", + "root_control", + "sink", + "concrete_implementation", + "evidence", +] as const; diff --git a/plugins/codex-security/mcp-app/src/finding-detail-schemas.ts b/plugins/codex-security/mcp-app/src/finding-detail-schemas.ts new file mode 100644 index 0000000000..728a258d3f --- /dev/null +++ b/plugins/codex-security/mcp-app/src/finding-detail-schemas.ts @@ -0,0 +1,119 @@ +import * as z from "zod/v4"; + +export function findingDetailSchemas( + text: z.ZodString, + path: z.ZodString, + line: z.ZodNumber, +) { + const slug = z.string().regex(new RegExp("^[a-z0-9][a-z0-9._/-]*$")); + const textList = z.array(text); + const findingAssessment = z.union([ + text, + z.looseObject({ + level: text.optional(), + rationale: text.optional(), + why: text.optional(), + }), + ]); + + const textOrSummary = z.union([ + text, + z.looseObject({ + summary: text.optional(), + source: text.optional(), + sink: text.optional(), + outcome: text.optional(), + transformations: textList.optional(), + evidenceRefs: textList.optional(), + evidence_refs: textList.optional(), + }), + ]); + + const findingReachability = z.union([ + text, + z.looseObject({ + summary: text.optional(), + attacker: text.optional(), + entrypoint: text.optional(), + source: text.optional(), + sink: text.optional(), + outcome: text.optional(), + preconditions: textList.optional(), + evidenceRefs: textList.optional(), + evidence_refs: textList.optional(), + }), + ]); + + const findingValidation = z.looseObject({ + assertions: textList.optional(), + counterEvidence: textList.optional(), + evidence: z.union([text, textList]).optional(), + evidenceRefs: textList.optional(), + evidence_refs: textList.optional(), + limitations: textList.optional(), + method: text.optional(), + status: z.union([text, z.null()]).optional(), + summary: text.optional(), + disposition: z.union([text, z.null()]).optional(), + result: z.union([text, z.null()]).optional(), + }); + + const findingAttackPath = z.looseObject({ + assumptions: textList.optional(), + blindspots: textList.optional(), + controls: textList.optional(), + dataFlow: textOrSummary.optional(), + data_flow: textOrSummary.optional(), + dataflow: textOrSummary.optional(), + evidenceRefs: textList.optional(), + evidence_refs: textList.optional(), + impact: z.union([findingAssessment, z.null()]).optional(), + likelihood: z.union([findingAssessment, z.null()]).optional(), + limitations: textList.optional(), + preconditions: textList.optional(), + reachability: findingReachability.optional(), + steps: textList.optional(), + summary: text.optional(), + }); + + return { + textList, + slug, + identity: z.looseObject({ anchor: slug, instance: slug.optional() }), + severity: z.looseObject({ + level: z.enum(["critical", "high", "medium", "low", "informational"]), + score: z.number().min(0).max(10).optional(), + scoringSystem: text.optional(), + vector: text.optional(), + rationale: text.optional(), + changeConditions: text.optional(), + }), + confidence: z.looseObject({ + level: z.enum(["high", "medium", "low"]), + rationale: text, + }), + location: z.looseObject({ + path, + startLine: line, + endLine: line.optional(), + role: text.optional(), + }), + codeEvidence: z.looseObject({ + id: slug, + label: text, + path, + startLine: line, + endLine: line.optional(), + language: text.optional(), + role: text.optional(), + code: text, + explanation: text, + }), + legacyCodeEvidence: z.looseObject({ id: text, code: text }), + findingAssessment, + textOrSummary, + findingReachability, + findingValidation, + findingAttackPath, + }; +} diff --git a/plugins/codex-security/mcp-app/src/helpers/normalize-candidates.ts b/plugins/codex-security/mcp-app/src/helpers/normalize-candidates.ts index 7314582932..9b60985595 100644 --- a/plugins/codex-security/mcp-app/src/helpers/normalize-candidates.ts +++ b/plugins/codex-security/mcp-app/src/helpers/normalize-candidates.ts @@ -1,3 +1,4 @@ +import { discoveryLocationRoles } from "../discovery-location-roles.js"; import { isRecord as object } from "../record.js"; import { decodeUtf8 } from "./utf8"; import { createHash, randomBytes } from "node:crypto"; @@ -22,15 +23,7 @@ import { } from "./resolve-security-md"; import { pathText, widePath } from "../../../native/windows-files.mjs"; -const roles = [ - "entrypoint", - "entrypoint/wrapper", - "source", - "root_control", - "sink", - "concrete_implementation", - "evidence", -]; +const roles: readonly string[] = discoveryLocationRoles; const fields = new Set([ "candidate_id", "cwe_ids", diff --git a/plugins/codex-security/mcp-app/src/threat-model-schema.ts b/plugins/codex-security/mcp-app/src/threat-model-schema.ts new file mode 100644 index 0000000000..cc3c50b6fe --- /dev/null +++ b/plugins/codex-security/mcp-app/src/threat-model-schema.ts @@ -0,0 +1,41 @@ +import * as z from "zod/v4"; + +export function threatModelSchemas( + textList: z.ZodArray, + content = textList.element, +) { + const scopeText = z.string().min(1); + const scope = z + .looseObject({ + includePaths: z.array(scopeText), + excludePaths: z.array(scopeText).optional(), + summary: scopeText.optional(), + }) + .describe( + "The modeled source scope, which may differ from the scan scope. Omit when unknown.", + ); + const markdown = z.looseObject({ + format: z.literal("markdown"), + content, + scope: scope.optional(), + origin: z + .enum(["generated", "provided", "reconciled", "recovered"]) + .optional(), + }); + const schema = z + .union([ + z.looseObject({ + summary: textList.element, + assets: textList.optional(), + trustBoundaries: textList.optional(), + attackerCapabilities: textList.optional(), + securityObjectives: textList.optional(), + assumptions: textList.optional(), + }), + markdown, + ]) + .describe( + "Retained threat-model content. Existing structured models remain supported; Markdown documents preserve their complete original body.", + ); + return { schema, markdown, scope }; +} diff --git a/plugins/codex-security/mcp-app/tests/scan-draft-fixture.ts b/plugins/codex-security/mcp-app/tests/scan-draft-fixture.ts index 4ac21d338a..af6b753777 100644 --- a/plugins/codex-security/mcp-app/tests/scan-draft-fixture.ts +++ b/plugins/codex-security/mcp-app/tests/scan-draft-fixture.ts @@ -8,7 +8,7 @@ export function workerDraft>( scanId, findings, coverage: { - completeness: "complete", + completeness: "complete" as const, surfaces: [], explicitExclusions: [], deferred: [], @@ -23,9 +23,9 @@ export function finding(id: string, repositoryPath: string) { identity: { anchor: id }, title: "Unsafe request output " + id, summary: "A request-controlled value reaches an HTML response.", - severity: { level: "high" }, + severity: { level: "high" as const }, confidence: { - level: "high", + level: "high" as const, rationale: "The source establishes reachability.", }, taxonomy: { category: "cross-site-scripting", cwe: ["CWE-79"] }, diff --git a/plugins/codex-security/mcp-app/tests/scan-draft-recovery-fixture.ts b/plugins/codex-security/mcp-app/tests/scan-draft-recovery-fixture.ts index 8f1822b9ea..8d6960e45e 100644 --- a/plugins/codex-security/mcp-app/tests/scan-draft-recovery-fixture.ts +++ b/plugins/codex-security/mcp-app/tests/scan-draft-recovery-fixture.ts @@ -84,6 +84,16 @@ export const claimToken = "19bfba38-0913-4bd7-86ef-134e9a4d9a42"; type Layout = "standard" | "diff" | "deep" | "worker"; +// Fixtures also exercise malformed and legacy inputs before runtime validation. +type DraftFixture = Omit< + ScanDraftInput, + "findings" | "coverage" | "threatModel" +> & { + findings: Record[]; + coverage: Record; + threatModel?: Record; +}; + export function draftFixture(root: string, layout: Layout) { const context: ArtifactContext = { root, @@ -117,7 +127,7 @@ export function draftFixture(root: string, layout: Layout) { const draft = ( coverage: { deferred?: unknown[]; [key: string]: unknown } = {}, complete = false, - ): ScanDraftInput => ({ + ): DraftFixture => ({ scanId, ...(layout === "worker" ? {} : { handoffClaimToken: claimToken }), complete, @@ -135,7 +145,7 @@ export function draftFixture(root: string, layout: Layout) { root, context, draft, - write: (input: ScanDraftInput) => + write: (input: unknown) => layout === "worker" ? draftApi.recordCodexSecurityWorkerScanDraft(context, input) : recordCodexSecurityScanDraft(context, input), diff --git a/plugins/codex-security/mcp-app/tests/test_artifact_contracts.ts b/plugins/codex-security/mcp-app/tests/test_artifact_contracts.ts new file mode 100644 index 0000000000..687340ca77 --- /dev/null +++ b/plugins/codex-security/mcp-app/tests/test_artifact_contracts.ts @@ -0,0 +1,158 @@ +import path from "node:path"; +import assert from "node:assert/strict"; +import Ajv2020 from "ajv/dist/2020.js"; +import { importSource } from "./import-module.ts"; +import { readJson } from "./support/json.ts"; + +const { artifactSchemaDocuments } = await importSource( + path.join(import.meta.dirname, "../src/artifact-contracts.ts"), +); +const ajv = new Ajv2020({ strict: false, validateFormats: false }); +for (const path of Object.keys(artifactSchemaDocuments)) { + ajv.addSchema( + await readJson(import.meta.dirname, `../../schemas/${path}.schema.json`), + ); +} +function jsonSchema(path: string, name: string) { + return ajv.getSchema( + `codex-security://schemas/${path}.schema.json#/$defs/${name}`, + )!; +} + +// Ledger readers expose only the first issue; preserve field precedence. +const attackPaths = + artifactSchemaDocuments["tools/candidate-attack-paths"].$defs; +const attackPath = { + dataflow: "Request reaches the operation.", + reachability: "The public handler calls it.", + counterevidence: "No intervening control.", + impact: "high", + likelihood: "high", +}; +for (const [schema, input, field] of [ + [ + artifactSchemaDocuments["definitions/discovery-candidate"].$defs + .discoveryCandidate, + {}, + "candidate_id", + ], + [ + artifactSchemaDocuments["tools/discovery-candidates"].$defs + .workbenchListCandidatesInput, + { scanId: 42, cursor: "bad" }, + "scanId", + ], + ...[ + [attackPaths.reportableAttackPath, "reportable"], + [attackPaths.ignoredAttackPath, "ignore"], + [attackPaths.deferredAttackPath, "deferred"], + ].map(([schema, decision]) => [ + schema, + { ...attackPath, decision }, + "severity", + ]), +] as const) { + const result = schema.safeParse(input); + assert.equal(result.success, false); + assert.deepEqual(result.error?.issues[0]?.path, [field]); +} + +// Custom validation exposes JSON-schema diagnostics, including legacy keywords. +for (const [path, name, value, message] of [ + [ + "tools/scan-draft", + "severity/properties/level", + null, + "must be equal to one of the allowed values", + ], + ...["disposition", "confidence"].map( + (field) => + [ + "tools/candidate-validations", + `validationRecord/properties/${field}`, + null, + "must be string", + ] as const, + ), + [ + "tools/scan-draft", + "finding/properties/findingId", + "invented", + "boolean schema is false", + ], + [ + "tools/scan-draft", + "finding/properties/writeup/properties/reportPath", + "invalid", + 'must match pattern "^findings/([a-z0-9][a-z0-9._-]*)/\\1\\.md$"', + ], +] as const) { + const validate = jsonSchema(path, name); + assert.equal(validate(value), false); + assert.equal(validate.errors?.[0]?.message, message); +} + +// SDK JSON consumers disable formats; native tools retain Zod's UUID and safe +// integer validation. Generated exports must preserve both existing contracts. +const common = artifactSchemaDocuments["definitions/artifact-common"].$defs; +const uuid = "61a20957-1be8-4ccf-8de8-eab4061e8cc3"; +for (const [value, valid] of [ + [uuid, true], + ["not-a-uuid", false], +] as const) { + assert.equal(common.scanId.safeParse(value).success, valid); + assert.equal( + jsonSchema("definitions/artifact-common", "scanId")(value), + true, + ); + assert.equal(jsonSchema("tools/scan-draft", "scanId")(value), true); +} +const location = artifactSchemaDocuments["tools/scan-draft"].$defs.location; +for (const [startLine, runtime, json] of [ + [1, true, true], + [0, false, false], + [1.5, false, false], + [Number.MAX_SAFE_INTEGER + 1, false, true], +] as const) { + const value = { path: "src/file.ts", startLine, endLine: startLine }; + assert.equal(location.safeParse(value).success, runtime); + assert.equal(jsonSchema("tools/scan-draft", "location")(value), json); +} +const reducer = + artifactSchemaDocuments["tools/deep-reducer"].$defs.reducerInputs; +const excessiveBudget = { maxBytes: Number.MAX_SAFE_INTEGER + 1 }; +assert.equal(reducer.safeParse(excessiveBudget).success, false); +assert.equal( + jsonSchema("tools/deep-reducer", "reducerInputs")(excessiveBudget), + false, +); + +// Uniqueness is checked on raw values before parsing the text items. +const sourceFindingIds = + artifactSchemaDocuments["tools/scan-draft"].$defs.finding.shape.provenance + .shape.sourceFindingIds; +for (const values of [ + [null, null], + [" ", " "], +]) { + const result = sourceFindingIds.safeParse(values); + assert.equal(result.success, false); + assert.deepEqual(result.error?.issues, [ + { + code: "custom", + path: [1], + message: + "Array items must be unique: element at index 1 duplicates the one at index 0", + }, + ]); +} + +// Named legacy definitions and cross-document aliases remain addressable. +for (const [path, { $defs }] of Object.entries( + artifactSchemaDocuments, +) as Array<[string, { $defs: Record }]>) { + for (const name of Object.keys($defs)) { + if (name !== "attackPathFields") + assert.ok(jsonSchema(path, name), `${path}: ${name}`); + } +} diff --git a/plugins/codex-security/mcp-app/tests/test_artifact_foundation.ts b/plugins/codex-security/mcp-app/tests/test_artifact_foundation.ts index bf0288776e..ed9976bd43 100644 --- a/plugins/codex-security/mcp-app/tests/test_artifact_foundation.ts +++ b/plugins/codex-security/mcp-app/tests/test_artifact_foundation.ts @@ -1,3 +1,4 @@ +import * as z from "zod/v4"; import { readJson } from "./support/json.ts"; import assert from "node:assert/strict"; import { spawn } from "node:child_process"; @@ -41,7 +42,7 @@ const writerApi = await importSource( }, ); const schemas = await importSource( - path.join(import.meta.dirname, "../src/artifact-schema-loader.ts"), + path.join(import.meta.dirname, "../src/artifact-contracts.ts"), ); const fixture = await realpath( await mkdtemp(path.join(tmpdir(), "codex-security-artifact-foundation-")), @@ -70,38 +71,12 @@ try { console.log("Codex Security compact artifact foundation tests passed"); async function testSchemaSourceOfTruth() { - const common = await readJson( - import.meta.dirname, - "../../schemas/definitions/artifact-common.schema.json", - ); - const fixtureDocument = { - $id: "codex-security://schemas/tools/artifact-foundation-fixture.schema.json", - $defs: { - request: { - type: "object", - properties: { - path: { $ref: common.$id + "#/$defs/repositoryPath" }, - candidateId: { $ref: common.$id + "#/$defs/candidateId" }, - }, - required: ["path", "candidateId"], - additionalProperties: false, - }, - }, - }; - const bundled = schemas.bundleArtifactSchema( - [common, fixtureDocument], - fixtureDocument.$id, - "request", - ); - assert.equal(JSON.stringify(bundled).includes("$ref"), false); - assert.equal(bundled.properties.path.type, "string"); - assert.equal(bundled.properties.candidateId.type, "string"); - - const validator = schemas.loadArtifactZodSchema( - [common, fixtureDocument], - fixtureDocument.$id, - "request", - ); + const common = + schemas.artifactSchemaDocuments["definitions/artifact-common"].$defs; + const validator = z.strictObject({ + path: common.repositoryPath, + candidateId: common.candidateId, + }); for (const repositoryPath of [ "src/index.ts", "./src/index.ts", @@ -145,19 +120,6 @@ async function testSchemaSourceOfTruth() { candidateId, ); } - assert.throws( - () => schemas.bundleArtifactSchema([common], "missing", "request"), - /Unknown Codex Security schema document/, - ); - assert.throws( - () => - schemas.bundleArtifactSchema( - [common, common], - common.$id, - "repositoryPath", - ), - /Duplicate Codex Security schema document/, - ); } async function testScanContext() { diff --git a/plugins/codex-security/mcp-app/tests/test_artifact_scan_draft.ts b/plugins/codex-security/mcp-app/tests/test_artifact_scan_draft.ts index f425f2d68f..1ffa786853 100644 --- a/plugins/codex-security/mcp-app/tests/test_artifact_scan_draft.ts +++ b/plugins/codex-security/mcp-app/tests/test_artifact_scan_draft.ts @@ -3,7 +3,6 @@ import { readJson, snapshotScanDraft, writeJsonLine } from "./support/json.ts"; import { mock } from "node:test"; import { temporaryDirectory } from "./support/temporary-directories.ts"; import type { ArtifactContext } from "../src/artifact-context.js"; -import type { ScanDraftInput } from "../src/artifact-scan-draft.js"; type FixtureFinding = Record & { provenance: Record; identity?: { anchor: string; instance?: string }; @@ -104,7 +103,7 @@ try { }); const rejectsDraft = ( - input: ScanDraftInput, + input: unknown, error: RegExp, boundContext: ArtifactContext = context, ) => assert.rejects(recordCodexSecurityScanDraft(boundContext, input), error); @@ -3022,10 +3021,7 @@ async function archiveFirstWorkerAttempt(output: string) { } // Projection cases below use the same fixture root but model independent scans. -async function recordFreshScanDraft( - context: ArtifactContext, - input: ScanDraftInput, -) { +async function recordFreshScanDraft(context: ArtifactContext, input: unknown) { await Promise.all([ ...[ "scan-manifest.json", diff --git a/plugins/codex-security/mcp-app/tests/test_artifact_scan_draft_recovery.ts b/plugins/codex-security/mcp-app/tests/test_artifact_scan_draft_recovery.ts index 548b29c53c..6a84bffad7 100644 --- a/plugins/codex-security/mcp-app/tests/test_artifact_scan_draft_recovery.ts +++ b/plugins/codex-security/mcp-app/tests/test_artifact_scan_draft_recovery.ts @@ -1,5 +1,4 @@ import { readJson, writeJson } from "./support/json.ts"; -import type { ScanDraftInput } from "../src/artifact-scan-draft.js"; import assert from "node:assert/strict"; import { execFile } from "node:child_process"; import { hash } from "node:crypto"; @@ -1007,7 +1006,7 @@ for (const layout of ["standard", "diff", "worker"] as const) { ); const pending = { id: "review", ...generic }; const closing = f.draft({ resolvedDeferred: [close(pending.id)] }, true); - const fail = (input: ScanDraftInput) => + const fail = (input: unknown) => interruptDraftWrite(path.join(f.root, destination), () => f.write(input), ); diff --git a/plugins/codex-security/mcp-app/tests/test_deep_scan_artifact_validation.ts b/plugins/codex-security/mcp-app/tests/test_deep_scan_artifact_validation.ts index 6fafbfb3fb..30de7057a8 100644 --- a/plugins/codex-security/mcp-app/tests/test_deep_scan_artifact_validation.ts +++ b/plugins/codex-security/mcp-app/tests/test_deep_scan_artifact_validation.ts @@ -390,7 +390,10 @@ async function testReducerValidation(root: string) { ), [previousCollisionA.summary, previousCollisionB.summary], ); - const referenced = (original: Record, refs: string[]) => ({ + const referenced = >( + original: Finding, + refs: string[], + ) => ({ ...original, provenance: { source: "local_plugin", sourceFindingIds: refs }, }); diff --git a/plugins/codex-security/mcp-app/tests/test_deep_scan_coordinator.ts b/plugins/codex-security/mcp-app/tests/test_deep_scan_coordinator.ts index ebe641a9b9..fbbf73b3db 100644 --- a/plugins/codex-security/mcp-app/tests/test_deep_scan_coordinator.ts +++ b/plugins/codex-security/mcp-app/tests/test_deep_scan_coordinator.ts @@ -3868,8 +3868,7 @@ async function testPublicationUsesAcceptedReducerSnapshot() { ); assert.equal(terminal?.status, "succeeded", terminal?.error); assert.equal( - (completed[0].findings[0].provenance as { candidateId: string }) - .candidateId, + completed[0].findings[0].provenance.candidateId, "accepted-finding", ); assert.equal(completed[0].coverage.completeness, "complete"); diff --git a/plugins/codex-security/schemas/definitions/artifact-common.schema.json b/plugins/codex-security/schemas/definitions/artifact-common.schema.json index 6a286b8fc5..f2dcd094e7 100644 --- a/plugins/codex-security/schemas/definitions/artifact-common.schema.json +++ b/plugins/codex-security/schemas/definitions/artifact-common.schema.json @@ -2,15 +2,8 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "codex-security://schemas/definitions/artifact-common.schema.json", "$defs": { - "nonEmptyText": { - "type": "string", - "minLength": 1, - "pattern": "\\S" - }, - "scanId": { - "type": "string", - "format": "uuid" - }, + "nonEmptyText": { "type": "string", "minLength": 1, "pattern": "\\S" }, + "scanId": { "type": "string", "format": "uuid" }, "repositoryPath": { "type": "string", "minLength": 1, diff --git a/plugins/codex-security/schemas/definitions/discovery-candidate.schema.json b/plugins/codex-security/schemas/definitions/discovery-candidate.schema.json index 7e128fb290..b30d7371bb 100644 --- a/plugins/codex-security/schemas/definitions/discovery-candidate.schema.json +++ b/plugins/codex-security/schemas/definitions/discovery-candidate.schema.json @@ -2,10 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "codex-security://schemas/definitions/discovery-candidate.schema.json", "$defs": { - "nonEmptyText": { - "type": "string", - "pattern": "\\S" - }, + "nonEmptyText": { "type": "string", "pattern": "\\S" }, "cweId": { "type": "string", "pattern": "^\\s*[Cc][Ww][Ee]-0*[1-9][0-9]*\\s*$" @@ -24,34 +21,61 @@ "rawDiscoveryLocation": { "type": "object", "properties": { - "path": { "$ref": "#/$defs/nonEmptyText" }, + "path": { + "$ref": "codex-security://schemas/definitions/discovery-candidate.schema.json#/$defs/nonEmptyText" + }, "start_line": { "type": "integer", "minimum": 1 }, "end_line": { "type": "integer", "minimum": 1 }, - "role": { "$ref": "#/$defs/locationRole" } + "role": { + "$ref": "codex-security://schemas/definitions/discovery-candidate.schema.json#/$defs/locationRole" + } }, "required": ["path", "start_line", "role"], "additionalProperties": false }, "normalizedDiscoveryLocation": { - "$ref": "#/$defs/rawDiscoveryLocation", - "required": ["path", "start_line", "end_line", "role"] + "type": "object", + "properties": { + "path": { + "$ref": "codex-security://schemas/definitions/discovery-candidate.schema.json#/$defs/nonEmptyText" + }, + "start_line": { "type": "integer", "minimum": 1 }, + "end_line": { "type": "integer", "minimum": 1 }, + "role": { + "$ref": "codex-security://schemas/definitions/discovery-candidate.schema.json#/$defs/locationRole" + } + }, + "required": ["path", "start_line", "end_line", "role"], + "additionalProperties": false }, "rawDiscoveryCandidate": { "type": "object", "properties": { "cwe_ids": { "type": "array", - "items": { "$ref": "#/$defs/cweId" } + "items": { + "$ref": "codex-security://schemas/definitions/discovery-candidate.schema.json#/$defs/cweId" + } }, "locations": { - "type": "array", "minItems": 1, - "items": { "$ref": "#/$defs/rawDiscoveryLocation" } + "type": "array", + "items": { + "$ref": "codex-security://schemas/definitions/discovery-candidate.schema.json#/$defs/rawDiscoveryLocation" + } + }, + "summary": { + "$ref": "codex-security://schemas/definitions/discovery-candidate.schema.json#/$defs/nonEmptyText" + }, + "evidence": { + "$ref": "codex-security://schemas/definitions/discovery-candidate.schema.json#/$defs/nonEmptyText" }, - "summary": { "$ref": "#/$defs/nonEmptyText" }, - "evidence": { "$ref": "#/$defs/nonEmptyText" }, - "context": { "$ref": "#/$defs/nonEmptyText" }, - "instance": { "$ref": "#/$defs/nonEmptyText" } + "context": { + "$ref": "codex-security://schemas/definitions/discovery-candidate.schema.json#/$defs/nonEmptyText" + }, + "instance": { + "$ref": "codex-security://schemas/definitions/discovery-candidate.schema.json#/$defs/nonEmptyText" + } }, "required": ["cwe_ids", "locations", "summary", "evidence"], "additionalProperties": false @@ -65,20 +89,27 @@ }, "cwe_ids": { "type": "array", - "items": { - "type": "string", - "pattern": "^CWE-[1-9][0-9]*$" - } + "items": { "type": "string", "pattern": "^CWE-[1-9][0-9]*$" } }, "locations": { - "type": "array", "minItems": 1, - "items": { "$ref": "#/$defs/normalizedDiscoveryLocation" } + "type": "array", + "items": { + "$ref": "codex-security://schemas/definitions/discovery-candidate.schema.json#/$defs/normalizedDiscoveryLocation" + } + }, + "summary": { + "$ref": "codex-security://schemas/definitions/discovery-candidate.schema.json#/$defs/nonEmptyText" + }, + "evidence": { + "$ref": "codex-security://schemas/definitions/discovery-candidate.schema.json#/$defs/nonEmptyText" + }, + "context": { + "$ref": "codex-security://schemas/definitions/discovery-candidate.schema.json#/$defs/nonEmptyText" }, - "summary": { "$ref": "#/$defs/nonEmptyText" }, - "evidence": { "$ref": "#/$defs/nonEmptyText" }, - "context": { "$ref": "#/$defs/nonEmptyText" }, - "instance": { "$ref": "#/$defs/nonEmptyText" } + "instance": { + "$ref": "codex-security://schemas/definitions/discovery-candidate.schema.json#/$defs/nonEmptyText" + } }, "required": [ "candidate_id", diff --git a/plugins/codex-security/schemas/tools/candidate-attack-paths.schema.json b/plugins/codex-security/schemas/tools/candidate-attack-paths.schema.json index 122edcb130..7610cab076 100644 --- a/plugins/codex-security/schemas/tools/candidate-attack-paths.schema.json +++ b/plugins/codex-security/schemas/tools/candidate-attack-paths.schema.json @@ -6,8 +6,6 @@ "$defs": { "input": { "type": "object", - "additionalProperties": false, - "required": ["scanId", "attackPaths"], "properties": { "scanId": { "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/scanId" @@ -15,34 +13,36 @@ "attackPaths": { "type": "array", "items": { - "$ref": "#/$defs/attackPathUpdate" + "$ref": "codex-security://schemas/tools/candidate-attack-paths.schema.json#/$defs/attackPathUpdate" } } - } + }, + "required": ["scanId", "attackPaths"], + "additionalProperties": false }, "attackPathUpdate": { "type": "object", - "additionalProperties": false, - "required": ["candidateId", "attackPath"], "properties": { "candidateId": { "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/candidateId" }, "attackPath": { - "$ref": "#/$defs/attackPath" + "$ref": "codex-security://schemas/tools/candidate-attack-paths.schema.json#/$defs/attackPath" } - } + }, + "required": ["candidateId", "attackPath"], + "additionalProperties": false }, "attackPath": { "oneOf": [ { - "$ref": "#/$defs/reportableAttackPath" + "$ref": "codex-security://schemas/tools/candidate-attack-paths.schema.json#/$defs/reportableAttackPath" }, { - "$ref": "#/$defs/ignoredAttackPath" + "$ref": "codex-security://schemas/tools/candidate-attack-paths.schema.json#/$defs/ignoredAttackPath" }, { - "$ref": "#/$defs/deferredAttackPath" + "$ref": "codex-security://schemas/tools/candidate-attack-paths.schema.json#/$defs/deferredAttackPath" } ] }, @@ -56,12 +56,8 @@ "counterevidence": { "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, - "impact": { - "enum": ["high", "medium", "low", "ignore", "unknown"] - }, - "likelihood": { - "enum": ["high", "medium", "low", "ignore", "unknown"] - }, + "impact": { "enum": ["high", "medium", "low", "ignore", "unknown"] }, + "likelihood": { "enum": ["high", "medium", "low", "ignore", "unknown"] }, "severity_rationale": { "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, @@ -71,7 +67,29 @@ }, "reportableAttackPath": { "type": "object", - "additionalProperties": true, + "properties": { + "decision": { "const": "reportable" }, + "dataflow": { + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" + }, + "reachability": { + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" + }, + "counterevidence": { + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" + }, + "impact": { "enum": ["high", "medium", "low", "ignore", "unknown"] }, + "likelihood": { + "enum": ["high", "medium", "low", "ignore", "unknown"] + }, + "severity": { "enum": ["critical", "high", "medium", "low"] }, + "severity_rationale": { + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" + }, + "change_conditions": { + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" + } + }, "required": [ "decision", "dataflow", @@ -83,21 +101,33 @@ "severity_rationale", "change_conditions" ], - "properties": { - "decision": { "const": "reportable" }, - "dataflow": { "$ref": "#/$defs/attackPathFields/dataflow" }, - "reachability": { "$ref": "#/$defs/attackPathFields/reachability" }, - "counterevidence": { "$ref": "#/$defs/attackPathFields/counterevidence" }, - "impact": { "$ref": "#/$defs/attackPathFields/impact" }, - "likelihood": { "$ref": "#/$defs/attackPathFields/likelihood" }, - "severity": { "enum": ["critical", "high", "medium", "low"] }, - "severity_rationale": { "$ref": "#/$defs/attackPathFields/severity_rationale" }, - "change_conditions": { "$ref": "#/$defs/attackPathFields/change_conditions" } - } + "additionalProperties": true }, "ignoredAttackPath": { "type": "object", - "additionalProperties": true, + "properties": { + "decision": { "const": "ignore" }, + "dataflow": { + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" + }, + "reachability": { + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" + }, + "counterevidence": { + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" + }, + "impact": { "enum": ["high", "medium", "low", "ignore", "unknown"] }, + "likelihood": { + "enum": ["high", "medium", "low", "ignore", "unknown"] + }, + "severity": { "const": "ignore" }, + "severity_rationale": { + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" + }, + "change_conditions": { + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" + } + }, "required": [ "decision", "dataflow", @@ -109,21 +139,38 @@ "severity_rationale", "change_conditions" ], - "properties": { - "decision": { "const": "ignore" }, - "dataflow": { "$ref": "#/$defs/attackPathFields/dataflow" }, - "reachability": { "$ref": "#/$defs/attackPathFields/reachability" }, - "counterevidence": { "$ref": "#/$defs/attackPathFields/counterevidence" }, - "impact": { "$ref": "#/$defs/attackPathFields/impact" }, - "likelihood": { "$ref": "#/$defs/attackPathFields/likelihood" }, - "severity": { "const": "ignore" }, - "severity_rationale": { "$ref": "#/$defs/attackPathFields/severity_rationale" }, - "change_conditions": { "$ref": "#/$defs/attackPathFields/change_conditions" } - } + "additionalProperties": true }, "deferredAttackPath": { "type": "object", - "additionalProperties": true, + "properties": { + "decision": { "const": "deferred" }, + "dataflow": { + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" + }, + "reachability": { + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" + }, + "counterevidence": { + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" + }, + "impact": { "enum": ["high", "medium", "low", "ignore", "unknown"] }, + "likelihood": { + "enum": ["high", "medium", "low", "ignore", "unknown"] + }, + "severity": { + "enum": ["critical", "high", "medium", "low", "unknown"] + }, + "severity_rationale": { + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" + }, + "change_conditions": { + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" + }, + "proof_gap": { + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" + } + }, "required": [ "decision", "dataflow", @@ -136,20 +183,7 @@ "change_conditions", "proof_gap" ], - "properties": { - "decision": { "const": "deferred" }, - "dataflow": { "$ref": "#/$defs/attackPathFields/dataflow" }, - "reachability": { "$ref": "#/$defs/attackPathFields/reachability" }, - "counterevidence": { "$ref": "#/$defs/attackPathFields/counterevidence" }, - "impact": { "$ref": "#/$defs/attackPathFields/impact" }, - "likelihood": { "$ref": "#/$defs/attackPathFields/likelihood" }, - "severity": { "enum": ["critical", "high", "medium", "low", "unknown"] }, - "severity_rationale": { "$ref": "#/$defs/attackPathFields/severity_rationale" }, - "change_conditions": { "$ref": "#/$defs/attackPathFields/change_conditions" }, - "proof_gap": { - "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" - } - } + "additionalProperties": true } } } diff --git a/plugins/codex-security/schemas/tools/candidate-validations.schema.json b/plugins/codex-security/schemas/tools/candidate-validations.schema.json index 6135af5c99..87c27fa97a 100644 --- a/plugins/codex-security/schemas/tools/candidate-validations.schema.json +++ b/plugins/codex-security/schemas/tools/candidate-validations.schema.json @@ -6,8 +6,6 @@ "$defs": { "input": { "type": "object", - "additionalProperties": false, - "required": ["scanId", "validations"], "properties": { "scanId": { "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/scanId" @@ -15,49 +13,37 @@ "validations": { "type": "array", "items": { - "$ref": "#/$defs/validationUpdate" + "$ref": "codex-security://schemas/tools/candidate-validations.schema.json#/$defs/validationUpdate" } } - } + }, + "required": ["scanId", "validations"], + "additionalProperties": false }, "validationUpdate": { "type": "object", - "additionalProperties": false, - "required": ["candidateId", "validation"], "properties": { "candidateId": { "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/candidateId" }, "validation": { - "$ref": "#/$defs/validationRecord" + "$ref": "codex-security://schemas/tools/candidate-validations.schema.json#/$defs/validationRecord" } - } + }, + "required": ["candidateId", "validation"], + "additionalProperties": false }, "validationRecord": { "type": "object", - "additionalProperties": true, - "required": [ - "disposition", - "method", - "confidence", - "confidence_rationale", - "rubric", - "evidence", - "counterevidence_or_proof_gap", - "remaining_uncertainty" - ], "properties": { "disposition": { - "type": "string", - "enum": ["reportable", "suppressed", "not_applicable", "deferred"] + "enum": ["reportable", "suppressed", "not_applicable", "deferred"], + "type": "string" }, "method": { "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, - "confidence": { - "type": "string", - "enum": ["high", "medium", "low"] - }, + "confidence": { "enum": ["high", "medium", "low"], "type": "string" }, "confidence_rationale": { "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, @@ -67,18 +53,22 @@ "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, { - "type": "object" + "type": "object", + "properties": {}, + "additionalProperties": true }, { - "type": "array", "minItems": 1, + "type": "array", "items": { "anyOf": [ { "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, { - "type": "object" + "type": "object", + "properties": {}, + "additionalProperties": true } ] } @@ -91,20 +81,16 @@ "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, { - "type": "array", "minItems": 1, + "type": "array", "items": { "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" } } ] }, - "counterevidence_or_proof_gap": { - "type": "string" - }, - "remaining_uncertainty": { - "type": "string" - }, + "counterevidence_or_proof_gap": { "type": "string" }, + "remaining_uncertainty": { "type": "string" }, "artifact_paths": { "type": "array", "items": { @@ -121,9 +107,31 @@ "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "preconditions": { - "$ref": "#/$defs/validationRecord/properties/evidence" + "anyOf": [ + { + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" + }, + { + "minItems": 1, + "type": "array", + "items": { + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" + } + } + ] } - } + }, + "required": [ + "disposition", + "method", + "confidence", + "confidence_rationale", + "rubric", + "evidence", + "counterevidence_or_proof_gap", + "remaining_uncertainty" + ], + "additionalProperties": true } } } diff --git a/plugins/codex-security/schemas/tools/deep-reducer.schema.json b/plugins/codex-security/schemas/tools/deep-reducer.schema.json index e459e97629..eea8bf7fc3 100644 --- a/plugins/codex-security/schemas/tools/deep-reducer.schema.json +++ b/plugins/codex-security/schemas/tools/deep-reducer.schema.json @@ -34,7 +34,7 @@ "description": "Omit or set true when submitting the finished aggregate." }, "scanId": { - "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/scanId" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/scanId" }, "handoffClaimToken": { "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/handoffClaimToken" @@ -46,7 +46,10 @@ "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/threatModel" }, "findings": { - "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/scanDraftInput/properties/findings" + "type": "array", + "items": { + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/finding" + } } }, "required": ["scanId", "findings"], diff --git a/plugins/codex-security/schemas/tools/discovery-candidates.schema.json b/plugins/codex-security/schemas/tools/discovery-candidates.schema.json index d758a43741..2f2f2e78f9 100644 --- a/plugins/codex-security/schemas/tools/discovery-candidates.schema.json +++ b/plugins/codex-security/schemas/tools/discovery-candidates.schema.json @@ -5,7 +5,9 @@ "workbenchRecordDiscoveryCandidatesInput": { "type": "object", "properties": { - "scanId": { "type": "string", "pattern": "\\S" }, + "scanId": { + "$ref": "codex-security://schemas/definitions/discovery-candidate.schema.json#/$defs/nonEmptyText" + }, "candidates": { "type": "array", "items": { @@ -19,11 +21,10 @@ "workbenchListCandidatesInput": { "type": "object", "properties": { - "scanId": { "type": "string", "pattern": "\\S" }, - "cursor": { - "type": "string", - "pattern": "^(?:0|[1-9][0-9]*)$" + "scanId": { + "$ref": "codex-security://schemas/definitions/discovery-candidate.schema.json#/$defs/nonEmptyText" }, + "cursor": { "type": "string", "pattern": "^(?:0|[1-9][0-9]*)$" }, "limit": { "type": "integer", "minimum": 1, "maximum": 1000 } }, "required": ["scanId"], diff --git a/plugins/codex-security/schemas/tools/review-items.schema.json b/plugins/codex-security/schemas/tools/review-items.schema.json index f94e37db65..04496b8274 100644 --- a/plugins/codex-security/schemas/tools/review-items.schema.json +++ b/plugins/codex-security/schemas/tools/review-items.schema.json @@ -24,15 +24,8 @@ "handoffClaimToken": { "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, - "cursor": { - "type": "string", - "pattern": "^(?:0|[1-9][0-9]*)$" - }, - "limit": { - "type": "integer", - "minimum": 1, - "maximum": 1000 - } + "cursor": { "type": "string", "pattern": "^(?:0|[1-9][0-9]*)$" }, + "limit": { "type": "integer", "minimum": 1, "maximum": 1000 } }, "required": ["scanId"], "additionalProperties": false diff --git a/plugins/codex-security/schemas/tools/scan-draft.schema.json b/plugins/codex-security/schemas/tools/scan-draft.schema.json index 283925401d..c22d37577a 100644 --- a/plugins/codex-security/schemas/tools/scan-draft.schema.json +++ b/plugins/codex-security/schemas/tools/scan-draft.schema.json @@ -2,6 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "codex-security://schemas/tools/scan-draft.schema.json", "title": "Codex Security semantic scan draft", + "$ref": "#/$defs/scanDraftInput", "$defs": { "text": { "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" @@ -9,7 +10,7 @@ "textList": { "type": "array", "items": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" } }, "scanId": { @@ -26,18 +27,12 @@ "type": "object", "properties": { "path": { - "$ref": "#/$defs/repositoryPath" - }, - "startLine": { - "type": "integer", - "minimum": 1 - }, - "endLine": { - "type": "integer", - "minimum": 1 + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/repositoryPath" }, + "startLine": { "type": "integer", "minimum": 1 }, + "endLine": { "type": "integer", "minimum": 1 }, "role": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" } }, "required": ["path", "startLine"], @@ -46,14 +41,8 @@ "identity": { "type": "object", "properties": { - "anchor": { - "type": "string", - "pattern": "^[a-z0-9][a-z0-9._/-]*$" - }, - "instance": { - "type": "string", - "pattern": "^[a-z0-9][a-z0-9._/-]*$" - } + "anchor": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._/-]*$" }, + "instance": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._/-]*$" } }, "required": ["anchor"], "additionalProperties": true @@ -64,22 +53,18 @@ "level": { "enum": ["critical", "high", "medium", "low", "informational"] }, - "score": { - "type": "number", - "minimum": 0, - "maximum": 10 - }, + "score": { "type": "number", "minimum": 0, "maximum": 10 }, "scoringSystem": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "vector": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "rationale": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "changeConditions": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" } }, "required": ["level"], @@ -88,11 +73,9 @@ "confidence": { "type": "object", "properties": { - "level": { - "enum": ["high", "medium", "low"] - }, + "level": { "enum": ["high", "medium", "low"] }, "rationale": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" } }, "required": ["level", "rationale"], @@ -102,12 +85,12 @@ "type": "object", "properties": { "category": { - "$ref": "#/$defs/text", - "description": "The actual primary broken security control, not a CWE identifier." + "description": "The actual primary broken security control, not a CWE identifier.", + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "cwe": { - "$ref": "#/$defs/textList", - "description": "Copy the reviewed candidate's exact cwe_ids array. Use an empty array when no CWE is established; do not invent one." + "description": "Copy the reviewed candidate's exact cwe_ids array. Use an empty array when no CWE is established; do not invent one.", + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" } }, "required": ["category", "cwe"], @@ -116,36 +99,27 @@ "codeEvidence": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^[a-z0-9][a-z0-9._/-]*$" - }, + "id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._/-]*$" }, "label": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "path": { - "$ref": "#/$defs/repositoryPath" - }, - "startLine": { - "type": "integer", - "minimum": 1 - }, - "endLine": { - "type": "integer", - "minimum": 1 + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/repositoryPath" }, + "startLine": { "type": "integer", "minimum": 1 }, + "endLine": { "type": "integer", "minimum": 1 }, "language": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "role": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "code": { - "$ref": "#/$defs/text", - "description": "The genuine, nonempty source snippet at this evidence location." + "description": "The genuine, nonempty source snippet at this evidence location.", + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "explanation": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" } }, "required": ["id", "label", "path", "startLine", "code", "explanation"], @@ -155,10 +129,10 @@ "type": "object", "properties": { "id": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "code": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" } }, "required": ["id", "code"], @@ -167,19 +141,19 @@ "findingAssessment": { "anyOf": [ { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, { "type": "object", "properties": { "level": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "rationale": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "why": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" } }, "additionalProperties": true @@ -189,31 +163,31 @@ "textOrSummary": { "anyOf": [ { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, { "type": "object", "properties": { "summary": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "source": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "sink": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "outcome": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "transformations": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" }, "evidenceRefs": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" }, "evidence_refs": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" } }, "additionalProperties": true @@ -223,37 +197,37 @@ "findingReachability": { "anyOf": [ { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, { "type": "object", "properties": { "summary": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "attacker": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "entrypoint": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "source": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "sink": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "outcome": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "preconditions": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" }, "evidenceRefs": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" }, "evidence_refs": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" } }, "additionalProperties": true @@ -264,64 +238,58 @@ "type": "object", "properties": { "assertions": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" }, "counterEvidence": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" }, "evidence": { "anyOf": [ { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" } ] }, "evidenceRefs": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" }, "evidence_refs": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" }, "limitations": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" }, "method": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "status": { "anyOf": [ { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, - { - "type": "null" - } + { "type": "null" } ] }, "summary": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "disposition": { "anyOf": [ { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, - { - "type": "null" - } + { "type": "null" } ] }, "result": { "anyOf": [ { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, - { - "type": "null" - } + { "type": "null" } ] } }, @@ -331,63 +299,59 @@ "type": "object", "properties": { "assumptions": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" }, "blindspots": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" }, "controls": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" }, "dataFlow": { - "$ref": "#/$defs/textOrSummary" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textOrSummary" }, "data_flow": { - "$ref": "#/$defs/textOrSummary" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textOrSummary" }, "dataflow": { - "$ref": "#/$defs/textOrSummary" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textOrSummary" }, "evidenceRefs": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" }, "evidence_refs": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" }, "impact": { "anyOf": [ { - "$ref": "#/$defs/findingAssessment" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/findingAssessment" }, - { - "type": "null" - } + { "type": "null" } ] }, "likelihood": { "anyOf": [ { - "$ref": "#/$defs/findingAssessment" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/findingAssessment" }, - { - "type": "null" - } + { "type": "null" } ] }, "limitations": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" }, "preconditions": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" }, "reachability": { - "$ref": "#/$defs/findingReachability" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/findingReachability" }, "steps": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" }, "summary": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" } }, "additionalProperties": true @@ -404,28 +368,28 @@ "description": "A stable lowercase vulnerability-family slug, such as prototype-pollution.json-patch; a CWE is taxonomy, not a rule ID." }, "identity": { - "$ref": "#/$defs/identity" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/identity" }, "title": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "summary": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "severity": { - "$ref": "#/$defs/severity" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/severity" }, "confidence": { - "$ref": "#/$defs/confidence" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/confidence" }, "taxonomy": { - "$ref": "#/$defs/taxonomy" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/taxonomy" }, "locations": { - "type": "array", "minItems": 1, + "type": "array", "items": { - "$ref": "#/$defs/location" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/location" } }, "writeup": { @@ -442,34 +406,34 @@ "codeEvidence": { "type": "array", "items": { - "$ref": "#/$defs/codeEvidence" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/codeEvidence" } }, "code_evidence": { "type": "array", "items": { - "$ref": "#/$defs/legacyCodeEvidence" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/legacyCodeEvidence" } }, "rootCause": { "anyOf": [ { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, { "type": "object", "properties": { "summary": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "code": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "language": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "evidenceRefs": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" } }, "required": ["summary"], @@ -480,25 +444,25 @@ "root_cause": { "anyOf": [ { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, { "type": "object", "properties": { "summary": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "code": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "language": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "evidenceRefs": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" }, "evidence_refs": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" } }, "additionalProperties": true @@ -506,33 +470,29 @@ ] }, "remediation": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "validation": { "anyOf": [ { - "$ref": "#/$defs/findingValidation" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/findingValidation" }, - { - "type": "null" - } + { "type": "null" } ] }, "attackPath": { "anyOf": [ { - "$ref": "#/$defs/findingAttackPath" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/findingAttackPath" }, - { - "type": "null" - } + { "type": "null" } ] }, "remediationTests": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" }, "preventiveControls": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" }, "provenance": { "type": "object", @@ -540,7 +500,7 @@ "sourceFindingIds": { "type": "array", "items": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "uniqueItems": true, "description": "Host-provided source finding references represented by this Deep reduction. Preserve every assigned reference exactly once." @@ -551,10 +511,11 @@ "type": "object", "properties": { "id": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "finding": { "type": "object", + "properties": {}, "additionalProperties": true } }, @@ -564,8 +525,8 @@ "description": "Original source payloads retained by the host for lossless semantic merges." }, "source": { - "$ref": "#/$defs/text", - "description": "The actual finding producer. Use local_plugin only for a finding discovered by this plugin." + "description": "The actual finding producer. Use local_plugin only for a finding discovered by this plugin.", + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" } }, "required": ["source"], @@ -573,6 +534,7 @@ }, "extensions": { "type": "object", + "properties": {}, "additionalProperties": true } }, @@ -595,49 +557,48 @@ "includePaths": false, "excludePaths": false, "summary": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "artifactsReviewed": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" }, "runtimeStatus": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "validationMode": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "context": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "limitations": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" } }, "additionalProperties": true }, "threatModel": { - "description": "Retained threat-model content. Existing structured models remain supported; Markdown documents preserve their complete original body.", "anyOf": [ { "type": "object", "properties": { "summary": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "assets": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" }, "trustBoundaries": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" }, "attackerCapabilities": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" }, "securityObjectives": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" }, "assumptions": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" } }, "required": ["summary"], @@ -646,37 +607,26 @@ { "type": "object", "properties": { - "format": { - "const": "markdown" - }, + "format": { "const": "markdown" }, "content": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "scope": { "type": "object", - "description": "The modeled source scope, which may differ from the scan scope. Omit when unknown.", "properties": { "includePaths": { "type": "array", - "items": { - "type": "string", - "minLength": 1 - } + "items": { "type": "string", "minLength": 1 } }, "excludePaths": { "type": "array", - "items": { - "type": "string", - "minLength": 1 - } + "items": { "type": "string", "minLength": 1 } }, - "summary": { - "type": "string", - "minLength": 1 - } + "summary": { "type": "string", "minLength": 1 } }, "required": ["includePaths"], - "additionalProperties": true + "additionalProperties": true, + "description": "The modeled source scope, which may differ from the scan scope. Omit when unknown." }, "origin": { "enum": ["generated", "provided", "reconciled", "recovered"] @@ -685,28 +635,26 @@ "required": ["format", "content"], "additionalProperties": true } - ] + ], + "description": "Retained threat-model content. Existing structured models remain supported; Markdown documents preserve their complete original body." }, "surface": { "type": "object", "properties": { - "id": { - "type": "string", - "pattern": "^[a-z0-9][a-z0-9._/-]*$" - }, + "id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._/-]*$" }, "label": { - "$ref": "#/$defs/text", - "description": "The meaningful name of the reviewed security surface." + "description": "The meaningful name of the reviewed security surface.", + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "disposition": { - "description": "The evidence-supported review result for this surface.", "enum": [ "reported", "no_issue_found", "rejected", "not_applicable", "needs_follow_up" - ] + ], + "description": "The evidence-supported review result for this surface." }, "receiptRefs": { "type": "array", @@ -717,10 +665,10 @@ } }, "riskArea": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "notes": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" } }, "required": ["label", "disposition"], @@ -737,14 +685,14 @@ "excludePaths": false, "receiptRefs": false, "completeness": { - "description": "Use partial if any work is deferred or any surface needs follow-up; use complete only when no such work remains.", - "enum": ["complete", "partial", "unknown"] + "enum": ["complete", "partial", "unknown"], + "description": "Use partial if any work is deferred or any surface needs follow-up; use complete only when no such work remains." }, "inventoryStrategy": false, "surfaces": { "type": "array", "items": { - "$ref": "#/$defs/surface" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/surface" } }, "explicitExclusions": { @@ -753,10 +701,10 @@ "type": "object", "properties": { "pattern": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "reason": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" } }, "required": ["pattern", "reason"], @@ -764,66 +712,63 @@ } }, "deferred": { - "description": "Each supplied id identifies one task and must be unique within this draft. Reuse saved IDs in later updates.", "type": "array", "items": { "type": "object", "properties": { "id": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "candidateId": { "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/candidateId" }, "reason": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "paths": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" }, "surfaceIds": { - "$ref": "#/$defs/textList" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/textList" } }, "required": ["reason"], "additionalProperties": true - } + }, + "description": "Each supplied id identifies one task and must be unique within this draft. Reuse saved IDs in later updates." }, "resolvedDeferred": { - "description": "On final Standard or diff drafts, close generic tasks by saved ID and completion reason. Reuse saved surface IDs for updates. Candidate tasks need findings or dispositions; redundant closure entries for those outcomes are ignored.", "type": "array", "items": { "type": "object", "properties": { "id": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "reason": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" } }, - "required": [ - "id", - "reason" - ], + "required": ["id", "reason"], "additionalProperties": false - } + }, + "description": "On final Standard or diff drafts, close generic tasks by saved ID and completion reason. Reuse saved surface IDs for updates. Candidate tasks need findings or dispositions; redundant closure entries for those outcomes are ignored." }, "openQuestions": { "type": "array", "items": { "anyOf": [ { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, { "type": "object", "properties": { "question": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" }, "followUpPrompt": { - "$ref": "#/$defs/text" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/nonEmptyText" } }, "required": ["question"], @@ -849,25 +794,25 @@ "description": "Set false to save progress without declaring this worker or parent audit finished. Omit or set true only for the terminal result. Put provisional candidates in coverage.deferred, not findings." }, "scanId": { - "$ref": "#/$defs/scanId" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/scanId" }, "handoffClaimToken": { - "$ref": "#/$defs/handoffClaimToken" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/handoffClaimToken" }, "scope": { - "$ref": "#/$defs/scope" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/scope" }, "threatModel": { - "$ref": "#/$defs/threatModel" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/threatModel" }, "findings": { "type": "array", "items": { - "$ref": "#/$defs/finding" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/finding" } }, "coverage": { - "$ref": "#/$defs/coverage" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/coverage" } }, "required": ["scanId", "findings", "coverage"], @@ -877,15 +822,14 @@ "type": "object", "properties": { "scanId": { - "$ref": "#/$defs/scanId" + "$ref": "codex-security://schemas/definitions/artifact-common.schema.json#/$defs/scanId" }, "handoffClaimToken": { - "$ref": "#/$defs/handoffClaimToken" + "$ref": "codex-security://schemas/tools/scan-draft.schema.json#/$defs/handoffClaimToken" } }, "required": ["scanId"], "additionalProperties": false } - }, - "$ref": "#/$defs/scanDraftInput" + } } From 0257a4e626dd457ad9669d368e06ca8f17657741 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Sat, 10 Oct 2026 23:59:44 +0000 Subject: [PATCH 2/2] refactor: generate canonical artifact schemas from Zod --- CONTRIBUTING.md | 11 +- .../mcp-app/scripts/canonical-schemas.mts | 325 ++++++++++ .../scripts/generate-artifact-schemas.mjs | 56 +- .../helpers/validate-patch-risk-assessment.ts | 28 +- .../mcp-app/src/patch-risk-schema.ts | 123 ++++ .../mcp-app/src/threat-model-schema.ts | 8 +- .../test_canonical_artifact_contracts.ts | 187 ++++++ .../schemas/coverage.schema.json | 205 ++----- .../schemas/findings.schema.json | 567 +++++------------- .../schemas/patch-risk-assessment.schema.json | 225 ++++--- .../schemas/scan-manifest.schema.json | 261 +++----- 11 files changed, 1073 insertions(+), 923 deletions(-) create mode 100644 plugins/codex-security/mcp-app/scripts/canonical-schemas.mts create mode 100644 plugins/codex-security/mcp-app/src/patch-risk-schema.ts create mode 100644 plugins/codex-security/mcp-app/tests/test_canonical_artifact_contracts.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 72383e5df5..be12dc8ba9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,12 +15,11 @@ packages and releases. Do not edit or commit files in that directory. See the [SDK testing guide](sdk/typescript/TESTING.md) for the generation and validation commands. -Compact artifact contracts are authored in `plugins/codex-security/mcp-app/src/artifact-contracts.ts`, -with shared finding and threat-model definitions in `finding-detail-schemas.ts` and -`threat-model-schema.ts`. The JSON files under `schemas/definitions` and `schemas/tools` -are generated compatibility resources for JSON consumers. After editing the contracts, -run `node plugins/codex-security/mcp-app/scripts/generate-artifact-schemas.mjs`. -The MCP `typecheck` command checks that the generated resources are current. +Compact artifact contracts are authored in `plugins/codex-security/mcp-app/src/artifact-contracts.ts`, with shared finding and threat-model definitions in `finding-detail-schemas.ts` and `threat-model-schema.ts`. +Canonical findings, scan-manifest, and coverage exports are defined in `mcp-app/scripts/canonical-schemas.mts`; patch-risk exports and their static type are defined in `mcp-app/src/patch-risk-schema.ts`. +The corresponding JSON schemas are generated compatibility resources. Canonical runtime validation continues to use the selected plugin's JSON schemas. +After editing the contracts, run `node plugins/codex-security/mcp-app/scripts/generate-artifact-schemas.mjs`. +The MCP `typecheck` command checks that the generated resources are current. SDK model generation remains based on the canonical JSON exports. Model-based evaluations live in [`evals/`](evals/README.md), outside the plugin source. Deterministic triage checks and the MCP reducer IPC regression remain diff --git a/plugins/codex-security/mcp-app/scripts/canonical-schemas.mts b/plugins/codex-security/mcp-app/scripts/canonical-schemas.mts new file mode 100644 index 0000000000..6925c64d03 --- /dev/null +++ b/plugins/codex-security/mcp-app/scripts/canonical-schemas.mts @@ -0,0 +1,325 @@ +import { surfaceDispositionSchema } from "../src/artifact-contracts.ts"; +import { threatModelSchemas } from "../src/threat-model-schema.ts"; +import * as z from "zod/v4"; +import { + patchRiskAssessment, + patchRiskDefinitions, +} from "../src/patch-risk-schema.ts"; +import { findingDetailSchemas } from "../src/finding-detail-schemas.ts"; + +// Generator-only definitions: canonical runtime validation uses the selected +// plugin's exported JSON Schemas in AJV and Python, including legacy type arrays. +function typeArray(schema: T, types: string[]): T { + z.globalRegistry.add(schema, { ...schema.meta(), type: types }); + return schema; +} +function pattern(source: string) { + return z.string().regex(new RegExp(source)).meta({ pattern: source }); +} + +const text = z.string().min(1); +const line = z.number().int().min(1).meta({ maximum: undefined }); +const { + textList, + slug, + identity, + severity, + confidence, + location, + codeEvidence, + legacyCodeEvidence, + findingAssessment, + textOrSummary, + findingReachability, + findingValidation, + findingAttackPath, +} = findingDetailSchemas(text, text, line); +// The flattened string-or-array contract needs inline items, not an array-only reference. +const validation = findingValidation.extend({ + evidence: z.union([text, z.array(text)]).optional(), +}); +for (const schema of [findingAssessment, textOrSummary, findingReachability]) + typeArray(schema, ["string", "object"]); +for (const field of ["status", "disposition", "result"] as const) + typeArray(validation.shape[field].unwrap(), ["string", "null"]); +typeArray(validation.shape.evidence.unwrap(), ["string", "array"]); +const assessment = typeArray(findingAttackPath.shape.impact.unwrap(), [ + "string", + "object", + "null", +]); +const attackPath = findingAttackPath.extend({ + impact: assessment.optional(), + likelihood: assessment.optional(), +}); +const rootCause = z.looseObject({ + summary: text, + evidenceRefs: textList.optional(), + code: text.optional(), + language: text.optional(), +}); +const finding = z.looseObject({ + findingId: z.string().regex(/^csf_[a-f0-9]{24}$/), + occurrenceId: z.string().regex(/^occ_[a-f0-9]{24}$/), + ruleId: slug, + identity, + fingerprints: z.looseObject({ + algorithm: z.literal("codex-security/v1"), + primary: pattern("^codex-security/v1:sha256:[a-f0-9]{64}$"), + }), + title: text, + summary: text, + severity, + confidence, + taxonomy: z.looseObject({ category: text, cwe: textList }), + locations: z.array(location).min(1), + writeup: z + .looseObject({ + reportPath: pattern("^findings/([a-z0-9][a-z0-9._-]*)/\\1\\.md$"), + }) + .optional(), + codeEvidence: z.array(codeEvidence).optional(), + code_evidence: typeArray(z.array(legacyCodeEvidence).nullable(), [ + "array", + "null", + ]).optional(), + rootCause: typeArray(z.union([rootCause, z.string()]), [ + "object", + "string", + ]).optional(), + root_cause: typeArray( + z.union([ + z.looseObject({ + summary: text.optional(), + evidenceRefs: textList.optional(), + evidence_refs: textList.optional(), + code: text.optional(), + language: text.optional(), + }), + z.string(), + z.null(), + ]), + ["object", "string", "null"], + ).optional(), + remediation: text, + validation: typeArray(validation.nullable(), ["object", "null"]).optional(), + attackPath: typeArray(attackPath.nullable(), ["object", "null"]).optional(), + remediationTests: textList.optional(), + preventiveControls: textList.optional(), + provenance: z.looseObject({ source: text }), + extensions: z + .looseObject({ + candidateId: text.optional(), + ledgerRowId: text.optional(), + reportId: text.optional(), + }) + .optional(), +}); + +const findingsDocument = z.looseObject({ + documentType: z.literal("codex-security.findings"), + schemaVersion: z.literal("1.0"), + scanId: text, + findings: z.array(finding), +}); + +const strings = z.array(z.string()); +const timestamp = z.string().meta({ format: "date-time" }); +const snapshotKinds = [ + "git_worktree", + "git_diff", + "directory_snapshot", +] as const; + +const target = z + .looseObject({ + kind: z.enum(["git_revision", ...snapshotKinds]), + targetId: text, + displayName: text, + remote: pattern("^(?![^:/?#]+://[^/?#]*@)[^?#]+$").optional(), + revision: z.string().optional(), + baseRevision: z.string().optional(), + headRevision: z.string().optional(), + snapshotDigest: pattern( + "^codex-security-snapshot/v1:sha256:[a-f0-9]{64}$", + ).optional(), + }) + .meta({ + allOf: [ + { + if: { properties: { kind: { const: "git_revision" } } }, + then: { required: ["revision"] }, + }, + { + if: { properties: { kind: { enum: snapshotKinds } } }, + then: { required: ["snapshotDigest"] }, + }, + ], + }); + +const { + schema: threatModel, + markdown, + scope, +} = threatModelSchemas(textList, text.regex(/^[\s\S]*\S[\s\S]*$/), textList); +for (const schema of [markdown, scope]) + z.globalRegistry.add(schema, { + ...z.globalRegistry.get(schema), + additionalProperties: true, + }); + +const scanManifest = z.looseObject({ + documentType: z.literal("codex-security.scan-manifest"), + schemaVersion: z.literal("1.0"), + scan: z + .looseObject({ + id: text, + producer: z.looseObject({ name: text, version: text }), + status: z.enum(["completed", "failed", "canceled", "interrupted"]), + startedAt: timestamp, + completedAt: timestamp, + sealedAt: timestamp, + target, + scope: z.looseObject({ + includePaths: strings, + excludePaths: strings, + summary: text.optional(), + artifactsReviewed: textList.optional(), + runtimeStatus: text.optional(), + validationMode: text.optional(), + context: text.optional(), + limitations: textList.optional(), + }), + threatModel: threatModel.optional(), + hardening: z + .looseObject({ portfolioPath: z.literal("hardening/hardening.md") }) + .optional(), + coverageRef: z.literal("coverage.json"), + findingsRef: z.literal("findings.json"), + artifacts: z + .array( + z.looseObject({ + path: pattern("^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\).+$").min( + 1, + ), + sha256: z.string().regex(/^[a-f0-9]{64}$/), + mediaType: text, + }), + ) + .min(1), + }) + .meta({ + allOf: ["findings.json", "coverage.json"].map((path) => ({ + properties: { + artifacts: { + contains: { + type: "object", + required: ["path"], + properties: { path: { const: path } }, + }, + minContains: 1, + maxContains: 1, + }, + }, + })), + }), +}); + +const coverageDocument = z + .looseObject({ + documentType: z.literal("codex-security.coverage"), + schemaVersion: z.literal("1.0"), + scanId: text, + mode: z.enum([ + "repository", + "scoped_path", + "diff", + "commit", + "branch_diff", + "working_tree", + "deep_repository", + ]), + completeness: z.enum(["complete", "partial", "unknown"]), + inventoryStrategy: z.enum([ + "repository", + "scoped_path", + "diff", + "directory", + "custom", + ]), + includePaths: strings, + excludePaths: strings, + surfaces: z.array( + z.looseObject({ + id: text, + label: text, + disposition: surfaceDispositionSchema, + receiptRefs: z.array( + pattern("^(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\)artifacts/.+$").min(1), + ), + riskArea: text.optional(), + notes: text.optional(), + }), + ), + explicitExclusions: z.array(z.looseObject({ pattern: text, reason: text })), + deferred: z.array( + z.looseObject({ + id: text, + reason: text, + paths: textList.optional(), + surfaceIds: textList.optional(), + }), + ), + resolvedDeferred: z + .array(z.strictObject({ id: text, reason: text })) + .describe( + "Completed generic review tasks, with their saved IDs and completion reasons.", + ) + .optional(), + openQuestions: z + .array(z.looseObject({ question: text, followUpPrompt: text.optional() })) + .optional(), + }) + .meta({ + allOf: [ + { + if: { + properties: { completeness: { const: "complete" } }, + required: ["completeness"], + }, + then: { + properties: { + deferred: { contains: {}, minContains: 0, maxContains: 0 }, + surfaces: { + contains: { + type: "object", + required: ["disposition"], + properties: { disposition: { const: "needs_follow_up" } }, + }, + minContains: 0, + maxContains: 0, + }, + }, + }, + }, + ], + }); + +export const canonicalSchemaDocuments = { + "patch-risk-assessment": { + title: "Patch risk assessment", + schema: patchRiskAssessment, + definitions: patchRiskDefinitions, + }, + findings: { + title: "Codex Security findings", + schema: findingsDocument, + definitions: { dataFlow: textOrSummary, assessment, stringList: textList }, + }, + "scan-manifest": { + title: "Codex Security sealed scan manifest", + schema: scanManifest, + definitions: { stringList: textList }, + }, + coverage: { title: "Codex Security scan coverage", schema: coverageDocument }, +}; diff --git a/plugins/codex-security/mcp-app/scripts/generate-artifact-schemas.mjs b/plugins/codex-security/mcp-app/scripts/generate-artifact-schemas.mjs index 1ada324025..f7f484bf98 100644 --- a/plugins/codex-security/mcp-app/scripts/generate-artifact-schemas.mjs +++ b/plugins/codex-security/mcp-app/scripts/generate-artifact-schemas.mjs @@ -6,7 +6,8 @@ import * as z from "zod/v4"; const bundled = await build({ stdin: { - contents: `export * from "../src/artifact-contracts.ts";`, + contents: `export * from "../src/artifact-contracts.ts"; + export * from "./canonical-schemas.mts";`, resolveDir: fileURLToPath(new URL(".", import.meta.url)), }, bundle: true, @@ -14,7 +15,11 @@ const bundled = await build({ format: "esm", write: false, }); -const { artifactSchemaDocuments, artifactJsonMetadata } = await import( +const { + artifactSchemaDocuments, + artifactJsonMetadata, + canonicalSchemaDocuments, +} = await import( `data:text/javascript;base64,${Buffer.from(bundled.outputFiles[0].contents).toString("base64")}` ); const registry = z.registry(); @@ -78,6 +83,53 @@ for (const [path, { $defs, ...header }] of Object.entries( JSON.stringify(schema) === '{"not":{}}' ? false : schema, ); } +for (const [path, { title, schema, definitions = {} }] of Object.entries( + canonicalSchemaDocuments, +)) { + const previousMetadata = new Map( + Object.values(definitions).map((definition) => [ + definition, + z.globalRegistry.get(definition), + ]), + ); + for (const [id, definition] of Object.entries(definitions)) + z.globalRegistry.add(definition, { + ...z.globalRegistry.get(definition), + id, + }); + await save( + path, + { + $id: `https://openai.com/codex-security/schemas/${path}.schema.json`, + title, + ...z.toJSONSchema(schema, { + io: "input", + override: ({ jsonSchema }) => { + if (jsonSchema.const !== undefined || jsonSchema.enum) + delete jsonSchema.type; + if (JSON.stringify(jsonSchema.additionalProperties) === "{}") + delete jsonSchema.additionalProperties; + }, + }), + }, + legacyTypeArrays, + ); + for (const [definition, metadata] of previousMetadata) { + z.globalRegistry.remove(definition); + if (metadata) z.globalRegistry.add(definition, metadata); + } +} +// Run after Zod resolves reused branches. Only explicitly marked canonical +// unions use this historical representation; other anyOf schemas stay intact. +function legacyTypeArrays(_key, schema) { + if (!Array.isArray(schema?.type) || !schema.anyOf) return schema; + const { anyOf, ...rest } = schema; + return Object.assign( + {}, + ...anyOf.map((part) => legacyTypeArrays("", part)), + rest, + ); +} async function save(path, schema, replacer) { const contents = await format(JSON.stringify(schema, replacer), { parser: "json", diff --git a/plugins/codex-security/mcp-app/src/helpers/validate-patch-risk-assessment.ts b/plugins/codex-security/mcp-app/src/helpers/validate-patch-risk-assessment.ts index 230029b3e8..db655b2629 100644 --- a/plugins/codex-security/mcp-app/src/helpers/validate-patch-risk-assessment.ts +++ b/plugins/codex-security/mcp-app/src/helpers/validate-patch-risk-assessment.ts @@ -1,3 +1,4 @@ +import type { PatchRiskAssessment } from "../patch-risk-schema.js"; import { parseArgs } from "node:util"; import Ajv2020 from "ajv/dist/2020.js"; import { decodeUtf8 } from "./utf8"; @@ -10,26 +11,8 @@ import { import { decodePosixBytes } from "./posix-path"; import { object, parseJson } from "./json"; -interface Assessment { - recommendation: "merge" | "revise" | "no_op" | "block" | "hold_for_evidence"; - workflowLabel: string; - impact: { rating: string }; - regressionLikelihood: { rating: string }; - regressionProtection: { rating: string; exactHeadChecksPassed: boolean }; - recoverability: { rating: string }; - confidence: { rating: string }; - applicability: { status: string }; - statusQuoRisk: { rating: string }; - affectedRuntimeRoots: string[]; - autoMergeExclusions: string[]; - materialBoundaries: { result: string }[]; - validation: { status: string }[]; - unknowns: { decisionCritical: boolean }[]; - evidencePlan: unknown[]; -} - const ajv = new Ajv2020({ strict: false }); -const assessmentValidator = ajv.compile(assessmentSchema); +const assessmentValidator = ajv.compile(assessmentSchema); const nonApplicable = new Set([ "no_live_effect", @@ -38,15 +21,14 @@ const nonApplicable = new Set([ "superseded", ]); -export function validatePatchRiskAssessment(value: unknown): string[] { - if (!object(value)) return ["assessment must be a JSON object"]; - if (!assessmentValidator(value)) +export function validatePatchRiskAssessment(assessment: unknown): string[] { + if (!object(assessment)) return ["assessment must be a JSON object"]; + if (!assessmentValidator(assessment)) return [ ajv.errorsText(assessmentValidator.errors, { dataVar: "patch-risk-assessment.schema", }), ]; - const assessment = value as unknown as Assessment; const { recommendation, workflowLabel, diff --git a/plugins/codex-security/mcp-app/src/patch-risk-schema.ts b/plugins/codex-security/mcp-app/src/patch-risk-schema.ts new file mode 100644 index 0000000000..449b1e984c --- /dev/null +++ b/plugins/codex-security/mcp-app/src/patch-risk-schema.ts @@ -0,0 +1,123 @@ +import * as z from "zod/v4"; + +// Generator-only schema and static type. Runtime validation retains AJV's +// exported JSON Schema, including uniqueness and minimum-property constraints. +const text = z.string().min(1); +const stringList = z.array(text).meta({ uniqueItems: true }); +const recommendation = z.enum([ + "merge", + "revise", + "no_op", + "block", + "hold_for_evidence", +]); +const rating = (values: T) => + z.strictObject({ + rating: z.enum(values), + rationale: text, + }); +const riskRating = rating(["low", "moderate", "high", "critical"]); +const recoveryRating = rating(["easy", "managed", "hard"]); +const confidenceRating = rating(["high", "moderate", "low"]); +const identifier = z.string().regex(/^[a-z0-9][a-z0-9_-]*$/); + +export const patchRiskDefinitions = { + nonEmptyString: text, + identifier, + stringList, + recommendation, + riskRating, + recoveryRating, + confidenceRating, +}; + +export const patchRiskAssessment = z.strictObject({ + schemaVersion: z.literal(1), + patch: z.strictObject({ + repository: text, + sourceType: z.enum(["pull_request_diff", "patch_file", "commit_range"]), + base: text, + head: text, + changedFiles: stringList, + sha256: z.string().regex(/^[0-9a-f]{64}$/), + }), + recommendation, + workflowLabel: z.enum([ + "auto_merge_candidate", + "human_review_required", + ...recommendation.exclude(["merge"]).options, + ]), + impact: riskRating, + regressionLikelihood: riskRating, + regressionProtection: z.strictObject({ + rating: z.enum(["strong", "partial", "none", "unknown"]), + rationale: text, + exactHeadChecksPassed: z.boolean(), + }), + recoverability: recoveryRating, + confidence: confidenceRating, + applicability: z.strictObject({ + status: z.enum([ + "confirmed", + "no_live_effect", + "wrong_owner", + "duplicate", + "superseded", + "unknown", + ]), + rationale: text, + }), + statusQuoRisk: rating(["low", "moderate", "high", "critical", "unknown"]), + autoMergeExclusions: z + .array( + z.enum([ + "privileged_boundary", + "migration", + "persistent_state", + "public_contract", + "architecture_specific_rollout", + "broad_shared_default", + "other", + ]), + ) + .meta({ uniqueItems: true }), + affectedRuntimeRoots: stringList, + importantCallers: stringList.optional(), + riskDrivers: stringList.optional(), + protectiveFactors: stringList.optional(), + materialBoundaries: z.array( + z.strictObject({ + id: identifier, + invariant: text, + runtimeRoot: text, + counterexample: text, + legitimateControl: text, + result: z.enum(["supported", "contradicted", "unresolved"]), + }), + ), + validation: z + .array( + z.strictObject({ + name: text, + status: z.enum(["passed", "failed", "skipped", "unavailable"]), + protects: text, + }), + ) + .min(1), + unknowns: z.array( + z.strictObject({ summary: text, decisionCritical: z.boolean() }), + ), + evidencePlan: z + .array( + z.strictObject({ + question: text, + action: text, + outcomes: z + .record(z.string(), recommendation) + .meta({ propertyNames: undefined, minProperties: 2 }), + }), + ) + .max(3), +}); + +export type PatchRiskAssessment = z.infer; diff --git a/plugins/codex-security/mcp-app/src/threat-model-schema.ts b/plugins/codex-security/mcp-app/src/threat-model-schema.ts index cc3c50b6fe..32b126fdf0 100644 --- a/plugins/codex-security/mcp-app/src/threat-model-schema.ts +++ b/plugins/codex-security/mcp-app/src/threat-model-schema.ts @@ -3,13 +3,13 @@ import * as z from "zod/v4"; export function threatModelSchemas( textList: z.ZodArray, content = textList.element, + scopeTextList = z.array(z.string().min(1)), ) { - const scopeText = z.string().min(1); const scope = z .looseObject({ - includePaths: z.array(scopeText), - excludePaths: z.array(scopeText).optional(), - summary: scopeText.optional(), + includePaths: scopeTextList, + excludePaths: scopeTextList.optional(), + summary: scopeTextList.element.optional(), }) .describe( "The modeled source scope, which may differ from the scan scope. Omit when unknown.", diff --git a/plugins/codex-security/mcp-app/tests/test_canonical_artifact_contracts.ts b/plugins/codex-security/mcp-app/tests/test_canonical_artifact_contracts.ts new file mode 100644 index 0000000000..bf413e12f8 --- /dev/null +++ b/plugins/codex-security/mcp-app/tests/test_canonical_artifact_contracts.ts @@ -0,0 +1,187 @@ +import assert from "node:assert/strict"; +import Ajv2020 from "ajv/dist/2020.js"; +import { readJson } from "./support/json.ts"; + +const ajv = new Ajv2020({ strict: false, validateFormats: false }); + +const canonical = Object.fromEntries( + await Promise.all( + ["findings", "scan-manifest", "coverage"].map(async (name) => { + const schema = await readJson( + import.meta.dirname, + `../../schemas/${name}.schema.json`, + ); + const example = await readJson( + import.meta.dirname, + `../../examples/completed-scan/${name}.json`, + ); + const validate = ajv.compile(schema); + assert.equal(validate(example), true, JSON.stringify(validate.errors)); + return [name, { schema, example, validate }]; + }), + ), +); + +// Persisted findings retain legacy values that semantic scan drafts reject. +// Compile the extracted Finding just as SDK deduplication does. +const findingSchema = { + ...canonical.findings.schema.properties.findings.items, + $defs: canonical.findings.schema.$defs, +}; +const validateFinding = ajv.compile(findingSchema); +const finding = canonical.findings.example.findings[0]; +for (const fields of [ + { title: " " }, + { + locations: [ + { path: "../legacy.ts", startLine: Number.MAX_SAFE_INTEGER + 1 }, + ], + }, + { rootCause: "", root_cause: null, code_evidence: null }, + { validation: { status: null, evidence: [" "] } }, + { attackPath: { dataflow: { extra: true }, impact: { why: " " } } }, + { provenance: { source: " ", sourceFindingIds: [1, 1] } }, +]) + assert.equal( + validateFinding({ ...finding, ...fields }), + true, + JSON.stringify(fields), + ); + +// Reused nested union branches must keep their constraints and error paths. +assert.equal( + validateFinding({ + ...finding, + attackPath: { likelihood: { rationale: 42 } }, + }), + false, +); +assert.deepEqual( + validateFinding.errors?.map(({ instancePath, keyword, message }) => ({ + instancePath, + keyword, + message, + })), + [ + { + instancePath: "/attackPath/likelihood/rationale", + keyword: "type", + message: "must be string", + }, + ], +); +assert.equal(validateFinding({ ...finding, code_evidence: 42 }), false); +assert.deepEqual(validateFinding.errors?.[0].params, { + type: ["array", "null"], +}); + +// Recovery retains the document definitions when validating extracted fragments. +for (const field of ["writeup", "remediationTests", "preventiveControls"]) { + const validate = ajv.compile({ + ...findingSchema.properties[field], + $defs: findingSchema.$defs, + }); + assert.equal( + validate( + field === "writeup" + ? { reportPath: "findings/example/example.md" } + : ["example"], + ), + true, + ); + assert.equal( + validate( + field === "writeup" + ? { reportPath: "findings/example/different.md" } + : [""], + ), + false, + ); +} + +const manifest = canonical["scan-manifest"]; +for (const kind of [ + "git_revision", + "git_worktree", + "git_diff", + "directory_snapshot", +]) { + const document = structuredClone(manifest.example); + document.scan.target.kind = kind; + delete document.scan.target.revision; + delete document.scan.target.snapshotDigest; + assert.equal(manifest.validate(document), false); + if (kind === "git_revision") document.scan.target.revision = ""; + else + document.scan.target.snapshotDigest = `codex-security-snapshot/v1:sha256:${"a".repeat(64)}`; + assert.equal(manifest.validate(document), true); +} +for (const path of ["findings.json", "coverage.json"]) { + const document = structuredClone(manifest.example); + const artifact = document.scan.artifacts.find( + (entry: { path: string }) => entry.path === path, + ); + document.scan.artifacts = document.scan.artifacts.filter( + (entry: { path: string }) => entry.path !== path, + ); + assert.equal(manifest.validate(document), false); + document.scan.artifacts.push(artifact, artifact); + assert.equal(manifest.validate(document), false); +} +const coverage = canonical.coverage; +for (const completeness of ["complete", "partial", "unknown"]) { + for (const fields of [ + { deferred: [{ id: "task", reason: "Review remains." }] }, + { + surfaces: [ + { + id: "surface", + label: "Source", + disposition: "needs_follow_up", + receiptRefs: [], + }, + ], + }, + ]) + assert.equal( + coverage.validate({ ...coverage.example, completeness, ...fields }), + completeness !== "complete", + ); +} + +// Shared named definitions remain available to consumers that extract fragments. +assert.deepEqual(Object.keys(findingSchema.$defs).sort(), [ + "assessment", + "dataFlow", + "stringList", +]); +assert.deepEqual(Object.keys(manifest.schema.$defs), ["stringList"]); + +// Patch-risk JSON keywords remain authoritative even when they are export metadata. +const patchRisk = await readJson( + import.meta.dirname, + "../../schemas/patch-risk-assessment.schema.json", +); +ajv.addSchema(patchRisk); +for (const [pointer, value, keyword] of [ + ["/$defs/stringList", ["same", "same"], "uniqueItems"], + [ + "/properties/autoMergeExclusions", + ["migration", "migration"], + "uniqueItems", + ], + [ + "/properties/evidencePlan/items/properties/outcomes", + { confirmed: "merge" }, + "minProperties", + ], + [ + "/properties/evidencePlan/items/properties/outcomes", + { confirmed: "merge", rejected: "unsupported" }, + "enum", + ], +] as const) { + const validate = ajv.getSchema(`${patchRisk.$id}#${pointer}`)!; + assert.equal(validate(value), false); + assert.equal(validate.errors?.[0]?.keyword, keyword); +} diff --git a/plugins/codex-security/schemas/coverage.schema.json b/plugins/codex-security/schemas/coverage.schema.json index 21e0877db0..60c7654720 100644 --- a/plugins/codex-security/schemas/coverage.schema.json +++ b/plugins/codex-security/schemas/coverage.schema.json @@ -1,32 +1,12 @@ { - "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://openai.com/codex-security/schemas/coverage.schema.json", "title": "Codex Security scan coverage", + "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", - "required": [ - "documentType", - "schemaVersion", - "scanId", - "mode", - "completeness", - "inventoryStrategy", - "includePaths", - "excludePaths", - "surfaces", - "explicitExclusions", - "deferred" - ], "properties": { - "documentType": { - "const": "codex-security.coverage" - }, - "schemaVersion": { - "const": "1.0" - }, - "scanId": { - "type": "string", - "minLength": 1 - }, + "documentType": { "const": "codex-security.coverage" }, + "schemaVersion": { "const": "1.0" }, + "scanId": { "type": "string", "minLength": 1 }, "mode": { "enum": [ "repository", @@ -38,53 +18,19 @@ "deep_repository" ] }, - "completeness": { - "enum": [ - "complete", - "partial", - "unknown" - ] - }, + "completeness": { "enum": ["complete", "partial", "unknown"] }, "inventoryStrategy": { - "enum": [ - "repository", - "scoped_path", - "diff", - "directory", - "custom" - ] - }, - "includePaths": { - "type": "array", - "items": { - "type": "string" - } - }, - "excludePaths": { - "type": "array", - "items": { - "type": "string" - } + "enum": ["repository", "scoped_path", "diff", "directory", "custom"] }, + "includePaths": { "type": "array", "items": { "type": "string" } }, + "excludePaths": { "type": "array", "items": { "type": "string" } }, "surfaces": { "type": "array", "items": { "type": "object", - "required": [ - "id", - "label", - "disposition", - "receiptRefs" - ], "properties": { - "id": { - "type": "string", - "minLength": 1 - }, - "label": { - "type": "string", - "minLength": 1 - }, + "id": { "type": "string", "minLength": 1 }, + "label": { "type": "string", "minLength": 1 }, "disposition": { "enum": [ "reported", @@ -102,143 +48,94 @@ "pattern": "^(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\)artifacts/.+$" } }, - "riskArea": { - "type": "string", - "minLength": 1 - }, - "notes": { - "type": "string", - "minLength": 1 - } - } + "riskArea": { "type": "string", "minLength": 1 }, + "notes": { "type": "string", "minLength": 1 } + }, + "required": ["id", "label", "disposition", "receiptRefs"] } }, "explicitExclusions": { "type": "array", "items": { "type": "object", - "required": [ - "pattern", - "reason" - ], "properties": { - "pattern": { - "type": "string", - "minLength": 1 - }, - "reason": { - "type": "string", - "minLength": 1 - } - } + "pattern": { "type": "string", "minLength": 1 }, + "reason": { "type": "string", "minLength": 1 } + }, + "required": ["pattern", "reason"] } }, "deferred": { "type": "array", "items": { "type": "object", - "required": [ - "id", - "reason" - ], "properties": { - "id": { - "type": "string", - "minLength": 1 - }, - "reason": { - "type": "string", - "minLength": 1 - }, + "id": { "type": "string", "minLength": 1 }, + "reason": { "type": "string", "minLength": 1 }, "paths": { "type": "array", - "items": { - "type": "string", - "minLength": 1 - } + "items": { "type": "string", "minLength": 1 } }, "surfaceIds": { "type": "array", - "items": { - "type": "string", - "minLength": 1 - } + "items": { "type": "string", "minLength": 1 } } - } + }, + "required": ["id", "reason"] } }, "resolvedDeferred": { - "description": "Completed generic review tasks, with their saved IDs and completion reasons.", "type": "array", "items": { "type": "object", "properties": { - "id": { - "type": "string", - "minLength": 1 - }, - "reason": { - "type": "string", - "minLength": 1 - } + "id": { "type": "string", "minLength": 1 }, + "reason": { "type": "string", "minLength": 1 } }, - "required": [ - "id", - "reason" - ], + "required": ["id", "reason"], "additionalProperties": false - } + }, + "description": "Completed generic review tasks, with their saved IDs and completion reasons." }, "openQuestions": { "type": "array", "items": { "type": "object", - "required": [ - "question" - ], "properties": { - "question": { - "type": "string", - "minLength": 1 - }, - "followUpPrompt": { - "type": "string", - "minLength": 1 - } - } + "question": { "type": "string", "minLength": 1 }, + "followUpPrompt": { "type": "string", "minLength": 1 } + }, + "required": ["question"] } } }, + "required": [ + "documentType", + "schemaVersion", + "scanId", + "mode", + "completeness", + "inventoryStrategy", + "includePaths", + "excludePaths", + "surfaces", + "explicitExclusions", + "deferred" + ], "allOf": [ { "if": { - "properties": { - "completeness": { - "const": "complete" - } - }, - "required": [ - "completeness" - ] + "properties": { "completeness": { "const": "complete" } }, + "required": ["completeness"] }, "then": { "properties": { - "deferred": { - "contains": {}, - "minContains": 0, - "maxContains": 0 - }, + "deferred": { "contains": {}, "minContains": 0, "maxContains": 0 }, "surfaces": { "contains": { "type": "object", - "required": [ - "disposition" - ], - "properties": { - "disposition": { - "const": "needs_follow_up" - } - } + "required": ["disposition"], + "properties": { "disposition": { "const": "needs_follow_up" } } }, "minContains": 0, "maxContains": 0 diff --git a/plugins/codex-security/schemas/findings.schema.json b/plugins/codex-security/schemas/findings.schema.json index c2012259f7..a1f09fb774 100644 --- a/plugins/codex-security/schemas/findings.schema.json +++ b/plugins/codex-security/schemas/findings.schema.json @@ -1,62 +1,22 @@ { - "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://openai.com/codex-security/schemas/findings.schema.json", "title": "Codex Security findings", + "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", - "required": [ - "documentType", - "schemaVersion", - "scanId", - "findings" - ], "properties": { - "documentType": { - "const": "codex-security.findings" - }, - "schemaVersion": { - "const": "1.0" - }, - "scanId": { - "type": "string", - "minLength": 1 - }, + "documentType": { "const": "codex-security.findings" }, + "schemaVersion": { "const": "1.0" }, + "scanId": { "type": "string", "minLength": 1 }, "findings": { "type": "array", "items": { "type": "object", - "required": [ - "findingId", - "occurrenceId", - "ruleId", - "identity", - "fingerprints", - "title", - "summary", - "severity", - "confidence", - "taxonomy", - "locations", - "remediation", - "provenance" - ], "properties": { - "findingId": { - "type": "string", - "pattern": "^csf_[a-f0-9]{24}$" - }, - "occurrenceId": { - "type": "string", - "pattern": "^occ_[a-f0-9]{24}$" - }, - "ruleId": { - "type": "string", - "pattern": "^[a-z0-9][a-z0-9._/-]*$" - }, + "findingId": { "type": "string", "pattern": "^csf_[a-f0-9]{24}$" }, + "occurrenceId": { "type": "string", "pattern": "^occ_[a-f0-9]{24}$" }, + "ruleId": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._/-]*$" }, "identity": { "type": "object", - "required": [ - "anchor" - ], "properties": { "anchor": { "type": "string", @@ -66,151 +26,94 @@ "type": "string", "pattern": "^[a-z0-9][a-z0-9._/-]*$" } - } + }, + "required": ["anchor"] }, "fingerprints": { "type": "object", - "required": [ - "algorithm", - "primary" - ], "properties": { - "algorithm": { - "const": "codex-security/v1" - }, + "algorithm": { "const": "codex-security/v1" }, "primary": { "type": "string", "pattern": "^codex-security/v1:sha256:[a-f0-9]{64}$" } - } - }, - "title": { - "type": "string", - "minLength": 1 - }, - "summary": { - "type": "string", - "minLength": 1 + }, + "required": ["algorithm", "primary"] }, + "title": { "type": "string", "minLength": 1 }, + "summary": { "type": "string", "minLength": 1 }, "severity": { "type": "object", - "required": [ - "level" - ], "properties": { "level": { - "enum": [ - "critical", - "high", - "medium", - "low", - "informational" - ] - }, - "score": { - "type": "number", - "minimum": 0, - "maximum": 10 - }, - "scoringSystem": { - "type": "string", - "minLength": 1 - }, - "vector": { - "type": "string", - "minLength": 1 - }, - "rationale": { - "type": "string", - "minLength": 1 - }, - "changeConditions": { - "type": "string", - "minLength": 1 - } - } + "enum": ["critical", "high", "medium", "low", "informational"] + }, + "score": { "type": "number", "minimum": 0, "maximum": 10 }, + "scoringSystem": { "type": "string", "minLength": 1 }, + "vector": { "type": "string", "minLength": 1 }, + "rationale": { "type": "string", "minLength": 1 }, + "changeConditions": { "type": "string", "minLength": 1 } + }, + "required": ["level"] }, "confidence": { "type": "object", - "required": [ - "level", - "rationale" - ], "properties": { - "level": { - "enum": [ - "high", - "medium", - "low" - ] - }, - "rationale": { - "type": "string", - "minLength": 1 - } - } + "level": { "enum": ["high", "medium", "low"] }, + "rationale": { "type": "string", "minLength": 1 } + }, + "required": ["level", "rationale"] }, "taxonomy": { "type": "object", - "required": [ - "category", - "cwe" - ], "properties": { - "category": { - "type": "string", - "minLength": 1 - }, - "cwe": { - "$ref": "#/$defs/stringList" - } - } + "category": { "type": "string", "minLength": 1 }, + "cwe": { "$ref": "#/$defs/stringList" } + }, + "required": ["category", "cwe"] }, "locations": { - "type": "array", "minItems": 1, + "type": "array", "items": { "type": "object", - "required": [ - "path", - "startLine" - ], "properties": { - "path": { - "type": "string", - "minLength": 1 - }, - "startLine": { - "type": "integer", - "minimum": 1 - }, - "endLine": { - "type": "integer", - "minimum": 1 - }, - "role": { - "type": "string", - "minLength": 1 - } - } + "path": { "type": "string", "minLength": 1 }, + "startLine": { "type": "integer", "minimum": 1 }, + "endLine": { "type": "integer", "minimum": 1 }, + "role": { "type": "string", "minLength": 1 } + }, + "required": ["path", "startLine"] } }, "writeup": { "type": "object", - "required": [ - "reportPath" - ], "properties": { "reportPath": { "type": "string", "pattern": "^findings/([a-z0-9][a-z0-9._-]*)/\\1\\.md$" } - } + }, + "required": ["reportPath"] }, "codeEvidence": { "type": "array", "items": { "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9._/-]*$" + }, + "label": { "type": "string", "minLength": 1 }, + "path": { "type": "string", "minLength": 1 }, + "startLine": { "type": "integer", "minimum": 1 }, + "endLine": { "type": "integer", "minimum": 1 }, + "language": { "type": "string", "minLength": 1 }, + "role": { "type": "string", "minLength": 1 }, + "code": { "type": "string", "minLength": 1 }, + "explanation": { "type": "string", "minLength": 1 } + }, "required": [ "id", "label", @@ -218,237 +121,78 @@ "startLine", "code", "explanation" - ], - "properties": { - "id": { - "type": "string", - "pattern": "^[a-z0-9][a-z0-9._/-]*$" - }, - "label": { - "type": "string", - "minLength": 1 - }, - "path": { - "type": "string", - "minLength": 1 - }, - "startLine": { - "type": "integer", - "minimum": 1 - }, - "endLine": { - "type": "integer", - "minimum": 1 - }, - "language": { - "type": "string", - "minLength": 1 - }, - "role": { - "type": "string", - "minLength": 1 - }, - "code": { - "type": "string", - "minLength": 1 - }, - "explanation": { - "type": "string", - "minLength": 1 - } - } + ] } }, "code_evidence": { - "type": [ - "array", - "null" - ], + "type": ["array", "null"], "items": { "type": "object", - "required": [ - "id", - "code" - ], "properties": { - "id": { - "type": "string", - "minLength": 1 - }, - "code": { - "type": "string", - "minLength": 1 - } - } + "id": { "type": "string", "minLength": 1 }, + "code": { "type": "string", "minLength": 1 } + }, + "required": ["id", "code"] } }, "rootCause": { - "type": [ - "object", - "string" - ], - "required": [ - "summary" - ], + "type": ["object", "string"], "properties": { - "summary": { - "type": "string", - "minLength": 1 - }, - "evidenceRefs": { - "$ref": "#/$defs/stringList" - }, - "code": { - "type": "string", - "minLength": 1 - }, - "language": { - "type": "string", - "minLength": 1 - } - } + "summary": { "type": "string", "minLength": 1 }, + "evidenceRefs": { "$ref": "#/$defs/stringList" }, + "code": { "type": "string", "minLength": 1 }, + "language": { "type": "string", "minLength": 1 } + }, + "required": ["summary"] }, "root_cause": { - "type": [ - "object", - "string", - "null" - ], + "type": ["object", "string", "null"], "properties": { - "summary": { - "type": "string", - "minLength": 1 - }, - "evidenceRefs": { - "$ref": "#/$defs/stringList" - }, - "evidence_refs": { - "$ref": "#/$defs/stringList" - }, - "code": { - "type": "string", - "minLength": 1 - }, - "language": { - "type": "string", - "minLength": 1 - } + "summary": { "type": "string", "minLength": 1 }, + "evidenceRefs": { "$ref": "#/$defs/stringList" }, + "evidence_refs": { "$ref": "#/$defs/stringList" }, + "code": { "type": "string", "minLength": 1 }, + "language": { "type": "string", "minLength": 1 } } }, - "remediation": { - "type": "string", - "minLength": 1 - }, + "remediation": { "type": "string", "minLength": 1 }, "validation": { - "type": [ - "object", - "null" - ], + "type": ["object", "null"], "properties": { - "assertions": { - "$ref": "#/$defs/stringList" - }, - "counterEvidence": { - "$ref": "#/$defs/stringList" - }, + "assertions": { "$ref": "#/$defs/stringList" }, + "counterEvidence": { "$ref": "#/$defs/stringList" }, "evidence": { - "type": [ - "string", - "array" - ], + "type": ["string", "array"], "minLength": 1, - "items": { - "type": "string", - "minLength": 1 - } - }, - "evidenceRefs": { - "$ref": "#/$defs/stringList" - }, - "evidence_refs": { - "$ref": "#/$defs/stringList" - }, - "limitations": { - "$ref": "#/$defs/stringList" - }, - "method": { - "type": "string", - "minLength": 1 - }, - "status": { - "type": [ - "string", - "null" - ], - "minLength": 1 - }, - "summary": { - "type": "string", - "minLength": 1 - }, - "disposition": { - "type": [ - "string", - "null" - ], - "minLength": 1 - }, - "result": { - "type": [ - "string", - "null" - ], - "minLength": 1 - } + "items": { "type": "string", "minLength": 1 } + }, + "evidenceRefs": { "$ref": "#/$defs/stringList" }, + "evidence_refs": { "$ref": "#/$defs/stringList" }, + "limitations": { "$ref": "#/$defs/stringList" }, + "method": { "type": "string", "minLength": 1 }, + "status": { "type": ["string", "null"], "minLength": 1 }, + "summary": { "type": "string", "minLength": 1 }, + "disposition": { "type": ["string", "null"], "minLength": 1 }, + "result": { "type": ["string", "null"], "minLength": 1 } } }, "attackPath": { - "type": [ - "object", - "null" - ], + "type": ["object", "null"], "properties": { - "assumptions": { - "$ref": "#/$defs/stringList" - }, - "blindspots": { - "$ref": "#/$defs/stringList" - }, - "controls": { - "$ref": "#/$defs/stringList" - }, - "dataFlow": { - "$ref": "#/$defs/dataFlow" - }, - "data_flow": { - "$ref": "#/$defs/dataFlow" - }, - "dataflow": { - "$ref": "#/$defs/dataFlow" - }, - "evidenceRefs": { - "$ref": "#/$defs/stringList" - }, - "evidence_refs": { - "$ref": "#/$defs/stringList" - }, - "impact": { - "$ref": "#/$defs/assessment" - }, - "likelihood": { - "$ref": "#/$defs/assessment" - }, - "limitations": { - "$ref": "#/$defs/stringList" - }, - "preconditions": { - "$ref": "#/$defs/stringList" - }, + "assumptions": { "$ref": "#/$defs/stringList" }, + "blindspots": { "$ref": "#/$defs/stringList" }, + "controls": { "$ref": "#/$defs/stringList" }, + "dataFlow": { "$ref": "#/$defs/dataFlow" }, + "data_flow": { "$ref": "#/$defs/dataFlow" }, + "dataflow": { "$ref": "#/$defs/dataFlow" }, + "evidenceRefs": { "$ref": "#/$defs/stringList" }, + "evidence_refs": { "$ref": "#/$defs/stringList" }, + "impact": { "$ref": "#/$defs/assessment" }, + "likelihood": { "$ref": "#/$defs/assessment" }, + "limitations": { "$ref": "#/$defs/stringList" }, + "preconditions": { "$ref": "#/$defs/stringList" }, "reachability": { - "type": [ - "string", - "object" - ], + "type": ["string", "object"], "minLength": 1, "properties": { "summary": { "type": "string", "minLength": 1 }, @@ -457,107 +201,76 @@ "source": { "type": "string", "minLength": 1 }, "sink": { "type": "string", "minLength": 1 }, "outcome": { "type": "string", "minLength": 1 }, - "preconditions": { - "$ref": "#/$defs/stringList" - }, - "evidenceRefs": { - "$ref": "#/$defs/stringList" - }, - "evidence_refs": { - "$ref": "#/$defs/stringList" - } + "preconditions": { "$ref": "#/$defs/stringList" }, + "evidenceRefs": { "$ref": "#/$defs/stringList" }, + "evidence_refs": { "$ref": "#/$defs/stringList" } } }, - "steps": { - "$ref": "#/$defs/stringList" - }, - "summary": { - "type": "string", - "minLength": 1 - } + "steps": { "$ref": "#/$defs/stringList" }, + "summary": { "type": "string", "minLength": 1 } } }, - "remediationTests": { - "$ref": "#/$defs/stringList" - }, - "preventiveControls": { - "$ref": "#/$defs/stringList" - }, + "remediationTests": { "$ref": "#/$defs/stringList" }, + "preventiveControls": { "$ref": "#/$defs/stringList" }, "provenance": { "type": "object", - "required": [ - "source" - ], - "properties": { - "source": { - "type": "string", - "minLength": 1 - } - } + "properties": { "source": { "type": "string", "minLength": 1 } }, + "required": ["source"] }, "extensions": { "type": "object", "properties": { - "candidateId": { - "type": "string", - "minLength": 1 - }, - "ledgerRowId": { - "type": "string", - "minLength": 1 - }, - "reportId": { - "type": "string", - "minLength": 1 - } + "candidateId": { "type": "string", "minLength": 1 }, + "ledgerRowId": { "type": "string", "minLength": 1 }, + "reportId": { "type": "string", "minLength": 1 } } } - } + }, + "required": [ + "findingId", + "occurrenceId", + "ruleId", + "identity", + "fingerprints", + "title", + "summary", + "severity", + "confidence", + "taxonomy", + "locations", + "remediation", + "provenance" + ] } } }, + "required": ["documentType", "schemaVersion", "scanId", "findings"], "$defs": { + "stringList": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, "dataFlow": { - "type": [ - "string", - "object" - ], + "type": ["string", "object"], "minLength": 1, "properties": { "summary": { "type": "string", "minLength": 1 }, "source": { "type": "string", "minLength": 1 }, "sink": { "type": "string", "minLength": 1 }, "outcome": { "type": "string", "minLength": 1 }, - "transformations": { - "$ref": "#/$defs/stringList" - }, - "evidenceRefs": { - "$ref": "#/$defs/stringList" - }, - "evidence_refs": { - "$ref": "#/$defs/stringList" - } + "transformations": { "$ref": "#/$defs/stringList" }, + "evidenceRefs": { "$ref": "#/$defs/stringList" }, + "evidence_refs": { "$ref": "#/$defs/stringList" } } }, "assessment": { - "type": [ - "string", - "object", - "null" - ], + "type": ["string", "object", "null"], "minLength": 1, "properties": { "level": { "type": "string", "minLength": 1 }, "rationale": { "type": "string", "minLength": 1 }, "why": { "type": "string", "minLength": 1 } } - }, - "stringList": { - "type": "array", - "items": { - "type": "string", - "minLength": 1 - } } } } diff --git a/plugins/codex-security/schemas/patch-risk-assessment.schema.json b/plugins/codex-security/schemas/patch-risk-assessment.schema.json index b89b932582..59cce52721 100644 --- a/plugins/codex-security/schemas/patch-risk-assessment.schema.json +++ b/plugins/codex-security/schemas/patch-risk-assessment.schema.json @@ -1,35 +1,22 @@ { - "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://openai.com/codex-security/schemas/patch-risk-assessment.schema.json", "title": "Patch risk assessment", + "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", - "additionalProperties": false, - "required": [ - "schemaVersion", - "patch", - "recommendation", - "workflowLabel", - "impact", - "regressionLikelihood", - "regressionProtection", - "recoverability", - "confidence", - "applicability", - "statusQuoRisk", - "autoMergeExclusions", - "affectedRuntimeRoots", - "materialBoundaries", - "validation", - "unknowns", - "evidencePlan" - ], "properties": { - "schemaVersion": { - "const": 1 - }, + "schemaVersion": { "const": 1 }, "patch": { "type": "object", - "additionalProperties": false, + "properties": { + "repository": { "$ref": "#/$defs/nonEmptyString" }, + "sourceType": { + "enum": ["pull_request_diff", "patch_file", "commit_range"] + }, + "base": { "$ref": "#/$defs/nonEmptyString" }, + "head": { "$ref": "#/$defs/nonEmptyString" }, + "changedFiles": { "$ref": "#/$defs/stringList" }, + "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" } + }, "required": [ "repository", "sourceType", @@ -38,21 +25,9 @@ "changedFiles", "sha256" ], - "properties": { - "repository": {"$ref": "#/$defs/nonEmptyString"}, - "sourceType": { - "enum": ["pull_request_diff", "patch_file", "commit_range"] - }, - "base": {"$ref": "#/$defs/nonEmptyString"}, - "head": {"$ref": "#/$defs/nonEmptyString"}, - "changedFiles": {"$ref": "#/$defs/stringList"}, - "sha256": { - "type": "string", - "pattern": "^[0-9a-f]{64}$" - } - } + "additionalProperties": false }, - "recommendation": {"$ref": "#/$defs/recommendation"}, + "recommendation": { "$ref": "#/$defs/recommendation" }, "workflowLabel": { "enum": [ "auto_merge_candidate", @@ -63,28 +38,22 @@ "hold_for_evidence" ] }, - "impact": {"$ref": "#/$defs/riskRating"}, - "regressionLikelihood": {"$ref": "#/$defs/riskRating"}, + "impact": { "$ref": "#/$defs/riskRating" }, + "regressionLikelihood": { "$ref": "#/$defs/riskRating" }, "regressionProtection": { "type": "object", - "additionalProperties": false, - "required": ["rating", "rationale", "exactHeadChecksPassed"], "properties": { - "rating": {"enum": ["strong", "partial", "none", "unknown"]}, - "rationale": {"$ref": "#/$defs/nonEmptyString"}, - "exactHeadChecksPassed": {"type": "boolean"} - } - }, - "recoverability": { - "$ref": "#/$defs/recoveryRating" - }, - "confidence": { - "$ref": "#/$defs/confidenceRating" + "rating": { "enum": ["strong", "partial", "none", "unknown"] }, + "rationale": { "$ref": "#/$defs/nonEmptyString" }, + "exactHeadChecksPassed": { "type": "boolean" } + }, + "required": ["rating", "rationale", "exactHeadChecksPassed"], + "additionalProperties": false }, + "recoverability": { "$ref": "#/$defs/recoveryRating" }, + "confidence": { "$ref": "#/$defs/confidenceRating" }, "applicability": { "type": "object", - "additionalProperties": false, - "required": ["status", "rationale"], "properties": { "status": { "enum": [ @@ -96,17 +65,21 @@ "unknown" ] }, - "rationale": {"$ref": "#/$defs/nonEmptyString"} - } + "rationale": { "$ref": "#/$defs/nonEmptyString" } + }, + "required": ["status", "rationale"], + "additionalProperties": false }, "statusQuoRisk": { "type": "object", - "additionalProperties": false, - "required": ["rating", "rationale"], "properties": { - "rating": {"enum": ["low", "moderate", "high", "critical", "unknown"]}, - "rationale": {"$ref": "#/$defs/nonEmptyString"} - } + "rating": { + "enum": ["low", "moderate", "high", "critical", "unknown"] + }, + "rationale": { "$ref": "#/$defs/nonEmptyString" } + }, + "required": ["rating", "rationale"], + "additionalProperties": false }, "autoMergeExclusions": { "type": "array", @@ -123,15 +96,22 @@ }, "uniqueItems": true }, - "affectedRuntimeRoots": {"$ref": "#/$defs/stringList"}, - "importantCallers": {"$ref": "#/$defs/stringList"}, - "riskDrivers": {"$ref": "#/$defs/stringList"}, - "protectiveFactors": {"$ref": "#/$defs/stringList"}, + "affectedRuntimeRoots": { "$ref": "#/$defs/stringList" }, + "importantCallers": { "$ref": "#/$defs/stringList" }, + "riskDrivers": { "$ref": "#/$defs/stringList" }, + "protectiveFactors": { "$ref": "#/$defs/stringList" }, "materialBoundaries": { "type": "array", "items": { "type": "object", - "additionalProperties": false, + "properties": { + "id": { "$ref": "#/$defs/identifier" }, + "invariant": { "$ref": "#/$defs/nonEmptyString" }, + "runtimeRoot": { "$ref": "#/$defs/nonEmptyString" }, + "counterexample": { "$ref": "#/$defs/nonEmptyString" }, + "legitimateControl": { "$ref": "#/$defs/nonEmptyString" }, + "result": { "enum": ["supported", "contradicted", "unresolved"] } + }, "required": [ "id", "invariant", @@ -140,73 +120,79 @@ "legitimateControl", "result" ], - "properties": { - "id": {"$ref": "#/$defs/identifier"}, - "invariant": {"$ref": "#/$defs/nonEmptyString"}, - "runtimeRoot": {"$ref": "#/$defs/nonEmptyString"}, - "counterexample": {"$ref": "#/$defs/nonEmptyString"}, - "legitimateControl": {"$ref": "#/$defs/nonEmptyString"}, - "result": {"enum": ["supported", "contradicted", "unresolved"]} - } + "additionalProperties": false } }, "validation": { - "type": "array", "minItems": 1, + "type": "array", "items": { "type": "object", - "additionalProperties": false, - "required": ["name", "status", "protects"], "properties": { - "name": {"$ref": "#/$defs/nonEmptyString"}, - "status": {"enum": ["passed", "failed", "skipped", "unavailable"]}, - "protects": {"$ref": "#/$defs/nonEmptyString"} - } + "name": { "$ref": "#/$defs/nonEmptyString" }, + "status": { "enum": ["passed", "failed", "skipped", "unavailable"] }, + "protects": { "$ref": "#/$defs/nonEmptyString" } + }, + "required": ["name", "status", "protects"], + "additionalProperties": false } }, "unknowns": { "type": "array", "items": { "type": "object", - "additionalProperties": false, - "required": ["summary", "decisionCritical"], "properties": { - "summary": {"$ref": "#/$defs/nonEmptyString"}, - "decisionCritical": {"type": "boolean"} - } + "summary": { "$ref": "#/$defs/nonEmptyString" }, + "decisionCritical": { "type": "boolean" } + }, + "required": ["summary", "decisionCritical"], + "additionalProperties": false } }, "evidencePlan": { - "type": "array", "maxItems": 3, + "type": "array", "items": { "type": "object", - "additionalProperties": false, - "required": ["question", "action", "outcomes"], "properties": { - "question": {"$ref": "#/$defs/nonEmptyString"}, - "action": {"$ref": "#/$defs/nonEmptyString"}, + "question": { "$ref": "#/$defs/nonEmptyString" }, + "action": { "$ref": "#/$defs/nonEmptyString" }, "outcomes": { "type": "object", - "additionalProperties": {"$ref": "#/$defs/recommendation"}, + "additionalProperties": { "$ref": "#/$defs/recommendation" }, "minProperties": 2 } - } + }, + "required": ["question", "action", "outcomes"], + "additionalProperties": false } } }, + "required": [ + "schemaVersion", + "patch", + "recommendation", + "workflowLabel", + "impact", + "regressionLikelihood", + "regressionProtection", + "recoverability", + "confidence", + "applicability", + "statusQuoRisk", + "autoMergeExclusions", + "affectedRuntimeRoots", + "materialBoundaries", + "validation", + "unknowns", + "evidencePlan" + ], + "additionalProperties": false, "$defs": { - "nonEmptyString": { - "type": "string", - "minLength": 1 - }, - "identifier": { - "type": "string", - "pattern": "^[a-z0-9][a-z0-9_-]*$" - }, + "nonEmptyString": { "type": "string", "minLength": 1 }, "stringList": { "type": "array", - "items": {"$ref": "#/$defs/nonEmptyString"}, + "items": { "$ref": "#/$defs/nonEmptyString" }, "uniqueItems": true }, "recommendation": { @@ -214,30 +200,31 @@ }, "riskRating": { "type": "object", - "additionalProperties": false, - "required": ["rating", "rationale"], "properties": { - "rating": {"enum": ["low", "moderate", "high", "critical"]}, - "rationale": {"$ref": "#/$defs/nonEmptyString"} - } + "rating": { "enum": ["low", "moderate", "high", "critical"] }, + "rationale": { "$ref": "#/$defs/nonEmptyString" } + }, + "required": ["rating", "rationale"], + "additionalProperties": false }, "recoveryRating": { "type": "object", - "additionalProperties": false, - "required": ["rating", "rationale"], "properties": { - "rating": {"enum": ["easy", "managed", "hard"]}, - "rationale": {"$ref": "#/$defs/nonEmptyString"} - } + "rating": { "enum": ["easy", "managed", "hard"] }, + "rationale": { "$ref": "#/$defs/nonEmptyString" } + }, + "required": ["rating", "rationale"], + "additionalProperties": false }, "confidenceRating": { "type": "object", - "additionalProperties": false, - "required": ["rating", "rationale"], "properties": { - "rating": {"enum": ["high", "moderate", "low"]}, - "rationale": {"$ref": "#/$defs/nonEmptyString"} - } - } + "rating": { "enum": ["high", "moderate", "low"] }, + "rationale": { "$ref": "#/$defs/nonEmptyString" } + }, + "required": ["rating", "rationale"], + "additionalProperties": false + }, + "identifier": { "type": "string", "pattern": "^[a-z0-9][a-z0-9_-]*$" } } } diff --git a/plugins/codex-security/schemas/scan-manifest.schema.json b/plugins/codex-security/schemas/scan-manifest.schema.json index bf614d85f1..5ca16a25ad 100644 --- a/plugins/codex-security/schemas/scan-manifest.schema.json +++ b/plugins/codex-security/schemas/scan-manifest.schema.json @@ -1,68 +1,31 @@ { - "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://openai.com/codex-security/schemas/scan-manifest.schema.json", "title": "Codex Security sealed scan manifest", + "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", - "required": ["documentType", "schemaVersion", "scan"], "properties": { - "documentType": { - "const": "codex-security.scan-manifest" - }, - "schemaVersion": { - "const": "1.0" - }, + "documentType": { "const": "codex-security.scan-manifest" }, + "schemaVersion": { "const": "1.0" }, "scan": { "type": "object", - "required": [ - "id", - "producer", - "status", - "startedAt", - "completedAt", - "sealedAt", - "target", - "scope", - "coverageRef", - "findingsRef", - "artifacts" - ], "properties": { - "id": { - "type": "string", - "minLength": 1 - }, + "id": { "type": "string", "minLength": 1 }, "producer": { "type": "object", - "required": ["name", "version"], "properties": { - "name": { - "type": "string", - "minLength": 1 - }, - "version": { - "type": "string", - "minLength": 1 - } - } + "name": { "type": "string", "minLength": 1 }, + "version": { "type": "string", "minLength": 1 } + }, + "required": ["name", "version"] }, "status": { "enum": ["completed", "failed", "canceled", "interrupted"] }, - "startedAt": { - "type": "string", - "format": "date-time" - }, - "completedAt": { - "type": "string", - "format": "date-time" - }, - "sealedAt": { - "type": "string", - "format": "date-time" - }, + "startedAt": { "type": "string", "format": "date-time" }, + "completedAt": { "type": "string", "format": "date-time" }, + "sealedAt": { "type": "string", "format": "date-time" }, "target": { "type": "object", - "required": ["kind", "targetId", "displayName"], "properties": { "kind": { "enum": [ @@ -72,44 +35,25 @@ "directory_snapshot" ] }, - "targetId": { - "type": "string", - "minLength": 1 - }, - "displayName": { - "type": "string", - "minLength": 1 - }, + "targetId": { "type": "string", "minLength": 1 }, + "displayName": { "type": "string", "minLength": 1 }, "remote": { "type": "string", "pattern": "^(?![^:/?#]+://[^/?#]*@)[^?#]+$" }, - "revision": { - "type": "string" - }, - "baseRevision": { - "type": "string" - }, - "headRevision": { - "type": "string" - }, + "revision": { "type": "string" }, + "baseRevision": { "type": "string" }, + "headRevision": { "type": "string" }, "snapshotDigest": { "type": "string", "pattern": "^codex-security-snapshot/v1:sha256:[a-f0-9]{64}$" } }, + "required": ["kind", "targetId", "displayName"], "allOf": [ { - "if": { - "properties": { - "kind": { - "const": "git_revision" - } - } - }, - "then": { - "required": ["revision"] - } + "if": { "properties": { "kind": { "const": "git_revision" } } }, + "then": { "required": ["revision"] } }, { "if": { @@ -119,86 +63,42 @@ } } }, - "then": { - "required": ["snapshotDigest"] - } + "then": { "required": ["snapshotDigest"] } } ] }, "scope": { "type": "object", - "required": ["includePaths", "excludePaths"], "properties": { - "includePaths": { - "type": "array", - "items": { - "type": "string" - } - }, - "excludePaths": { - "type": "array", - "items": { - "type": "string" - } - }, - "summary": { - "type": "string", - "minLength": 1 - }, - "artifactsReviewed": { - "$ref": "#/$defs/stringList" - }, - "runtimeStatus": { - "type": "string", - "minLength": 1 - }, - "validationMode": { - "type": "string", - "minLength": 1 - }, - "context": { - "type": "string", - "minLength": 1 - }, - "limitations": { - "$ref": "#/$defs/stringList" - } - } + "includePaths": { "type": "array", "items": { "type": "string" } }, + "excludePaths": { "type": "array", "items": { "type": "string" } }, + "summary": { "type": "string", "minLength": 1 }, + "artifactsReviewed": { "$ref": "#/$defs/stringList" }, + "runtimeStatus": { "type": "string", "minLength": 1 }, + "validationMode": { "type": "string", "minLength": 1 }, + "context": { "type": "string", "minLength": 1 }, + "limitations": { "$ref": "#/$defs/stringList" } + }, + "required": ["includePaths", "excludePaths"] }, "threatModel": { - "description": "Retained threat-model content. Existing structured models remain supported; Markdown documents preserve their complete original body.", "anyOf": [ { "type": "object", - "required": ["summary"], "properties": { - "summary": { - "type": "string", - "minLength": 1 - }, - "assets": { - "$ref": "#/$defs/stringList" - }, - "trustBoundaries": { - "$ref": "#/$defs/stringList" - }, - "attackerCapabilities": { - "$ref": "#/$defs/stringList" - }, - "securityObjectives": { - "$ref": "#/$defs/stringList" - }, - "assumptions": { - "$ref": "#/$defs/stringList" - } - } + "summary": { "type": "string", "minLength": 1 }, + "assets": { "$ref": "#/$defs/stringList" }, + "trustBoundaries": { "$ref": "#/$defs/stringList" }, + "attackerCapabilities": { "$ref": "#/$defs/stringList" }, + "securityObjectives": { "$ref": "#/$defs/stringList" }, + "assumptions": { "$ref": "#/$defs/stringList" } + }, + "required": ["summary"] }, { "type": "object", "properties": { - "format": { - "const": "markdown" - }, + "format": { "const": "markdown" }, "content": { "type": "string", "minLength": 1, @@ -206,21 +106,14 @@ }, "scope": { "type": "object", - "description": "The modeled source scope, which may differ from the scan scope. Omit when unknown.", "properties": { - "includePaths": { - "$ref": "#/$defs/stringList" - }, - "excludePaths": { - "$ref": "#/$defs/stringList" - }, - "summary": { - "type": "string", - "minLength": 1 - } + "includePaths": { "$ref": "#/$defs/stringList" }, + "excludePaths": { "$ref": "#/$defs/stringList" }, + "summary": { "type": "string", "minLength": 1 } }, "required": ["includePaths"], - "additionalProperties": true + "additionalProperties": true, + "description": "The modeled source scope, which may differ from the scan scope. Omit when unknown." }, "origin": { "enum": ["generated", "provided", "reconciled", "recovered"] @@ -229,47 +122,49 @@ "required": ["format", "content"], "additionalProperties": true } - ] + ], + "description": "Retained threat-model content. Existing structured models remain supported; Markdown documents preserve their complete original body." }, "hardening": { "type": "object", - "required": ["portfolioPath"], "properties": { - "portfolioPath": { - "const": "hardening/hardening.md" - } - } - }, - "coverageRef": { - "const": "coverage.json" - }, - "findingsRef": { - "const": "findings.json" + "portfolioPath": { "const": "hardening/hardening.md" } + }, + "required": ["portfolioPath"] }, + "coverageRef": { "const": "coverage.json" }, + "findingsRef": { "const": "findings.json" }, "artifacts": { - "type": "array", "minItems": 1, + "type": "array", "items": { "type": "object", - "required": ["path", "sha256", "mediaType"], "properties": { "path": { "type": "string", "minLength": 1, "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\).+$" }, - "sha256": { - "type": "string", - "pattern": "^[a-f0-9]{64}$" - }, - "mediaType": { - "type": "string", - "minLength": 1 - } - } + "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, + "mediaType": { "type": "string", "minLength": 1 } + }, + "required": ["path", "sha256", "mediaType"] } } }, + "required": [ + "id", + "producer", + "status", + "startedAt", + "completedAt", + "sealedAt", + "target", + "scope", + "coverageRef", + "findingsRef", + "artifacts" + ], "allOf": [ { "properties": { @@ -277,11 +172,7 @@ "contains": { "type": "object", "required": ["path"], - "properties": { - "path": { - "const": "findings.json" - } - } + "properties": { "path": { "const": "findings.json" } } }, "minContains": 1, "maxContains": 1 @@ -294,11 +185,7 @@ "contains": { "type": "object", "required": ["path"], - "properties": { - "path": { - "const": "coverage.json" - } - } + "properties": { "path": { "const": "coverage.json" } } }, "minContains": 1, "maxContains": 1 @@ -308,13 +195,11 @@ ] } }, + "required": ["documentType", "schemaVersion", "scan"], "$defs": { "stringList": { "type": "array", - "items": { - "type": "string", - "minLength": 1 - } + "items": { "type": "string", "minLength": 1 } } } }