diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cfb760909..dc6dc0280 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. `pnpm --dir sdk/typescript run generate:models` also copies workbench argument encoding and executable path handling from the plugin source into the SDK. Edit the 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 000000000..0094f93e9 --- /dev/null +++ b/plugins/codex-security/mcp-app/scripts/canonical-schemas.mts @@ -0,0 +1,315 @@ +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, + findingAssessment, + textOrSummary: sourceTextOrSummary, + findingReachability: sourceFindingReachability, + findingValidation, + findingAttackPath, +} = findingDetailSchemas(text, text, line); +const legacyCodeEvidence = z.looseObject({ id: text, code: text }); +const textOrSummary = z.union([ + text, + sourceTextOrSummary.options[1].extend({ evidence_refs: textList.optional() }), +]); +const findingReachability = z.union([ + text, + sourceFindingReachability.options[1].extend({ + evidence_refs: textList.optional(), + }), +]); +// 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(), + evidence_refs: textList.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({ + dataFlow: textOrSummary.optional(), + data_flow: textOrSummary.optional(), + dataflow: textOrSummary.optional(), + evidence_refs: textList.optional(), + impact: assessment.optional(), + likelihood: assessment.optional(), + reachability: findingReachability.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", + ]), + 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(), +}); + +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 35c8c3d47..95cbf0a60 100644 --- a/plugins/codex-security/mcp-app/scripts/generate-artifact-schemas.mjs +++ b/plugins/codex-security/mcp-app/scripts/generate-artifact-schemas.mjs @@ -7,7 +7,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, @@ -15,7 +16,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(); @@ -79,6 +84,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 230029b3e..db655b262 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 000000000..449b1e984 --- /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 cc3c50b6f..32b126fdf 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 000000000..b37f8e772 --- /dev/null +++ b/plugins/codex-security/mcp-app/tests/test_canonical_artifact_contracts.ts @@ -0,0 +1,193 @@ +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: [" "] } }, + { validation: { evidence_refs: ["legacy evidence"] } }, + { code_evidence: [{ id: "legacy", code: "example()" }] }, + { + attackPath: { + dataFlow: { evidence_refs: ["legacy evidence"] }, + data_flow: { evidence_refs: ["legacy evidence"] }, + dataflow: { evidence_refs: ["legacy evidence"] }, + reachability: { evidence_refs: ["legacy evidence"] }, + evidence_refs: ["legacy 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 fields of [ + { deferred: [{ id: "task", reason: "Review remains." }] }, + { + surfaces: [ + { + id: "surface", + label: "Source", + disposition: "needs_follow_up", + receiptRefs: [], + }, + ], + }, +]) + assert.equal(coverage.validate({ ...coverage.example, ...fields }), true); + +// 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 11c345d1f..033023fc8 100644 --- a/plugins/codex-security/schemas/coverage.schema.json +++ b/plugins/codex-security/schemas/coverage.schema.json @@ -1,31 +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", - "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,45 +19,17 @@ ] }, "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", @@ -94,111 +47,77 @@ "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", + "inventoryStrategy", + "includePaths", + "excludePaths", + "surfaces", + "explicitExclusions", + "deferred" + ] } diff --git a/plugins/codex-security/schemas/findings.schema.json b/plugins/codex-security/schemas/findings.schema.json index c2012259f..a1f09fb77 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 b89b93258..59cce5272 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 bf614d85f..5ca16a25a 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 } } } }