@quantakrypto/core
Advanced tools
+15
-0
| import type { ScanResult } from "./types.js"; | ||
| import type { CryptoPolicy, PolicyMapping } from "./policy.js"; | ||
| import type { MandateEvaluation } from "./mandates.js"; | ||
| /** Stable per-finding record for the evidence body (deterministic per commit). */ | ||
@@ -29,2 +30,10 @@ export interface EvidenceFinding { | ||
| policyMapping?: PolicyMapping; | ||
| /** | ||
| * Compliance-mandate verdicts (`--mandate`), present only when mandates were | ||
| * evaluated. DATE-PINNED for reproducibility: its `now` is stored as a plain | ||
| * `YYYY-MM-DD` (not the volatile scan timestamp) so the same scan of the same | ||
| * commit ON THE SAME DAY yields the same attestation hash, while a genuinely | ||
| * different compliance date (a passed deadline) correctly changes it. | ||
| */ | ||
| mandateMapping?: MandateEvaluation; | ||
| cbom: unknown; | ||
@@ -58,2 +67,8 @@ attestation: { | ||
| policy?: CryptoPolicy; | ||
| /** | ||
| * Optional compliance-mandate evaluation ({@link evaluateMandates}) — adds the | ||
| * `mandateMapping` block. Date-pinned into the hashed body (see | ||
| * {@link ReadinessReport.mandateMapping}). | ||
| */ | ||
| mandate?: MandateEvaluation; | ||
| } | ||
@@ -60,0 +75,0 @@ /** |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"evidence.d.ts","sourceRoot":"","sources":["../src/evidence.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAI7C,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE/D,kFAAkF;AAClF,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,wBAAwB,CAAC;IACrC,WAAW,EAAE,CAAC,CAAC;IACf,OAAO,EAAE;QACP,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;QAC1B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,IAAI,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,SAAS,EAAE,UAAU,CAAC,WAAW,CAAC,CAAC;IACnC,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,0EAA0E;IAC1E,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE;QACX,+EAA+E;QAC/E,WAAW,EAAE,MAAM,CAAC;QACpB;;;;WAIG;QACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB;;;WAGG;QACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,+EAA+E;QAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,6EAA6E;QAC7E,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,CAAC;CACH;AAeD,MAAM,WAAW,sBAAsB;IACrC,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,UAAU,EAClB,IAAI,GAAE,sBAA2B,GAChC,eAAe,CA2CjB;AAED,mDAAmD;AACnD,MAAM,WAAW,qBAAqB;IACpC,oFAAoF;IACpF,KAAK,EAAE,OAAO,CAAC;IACf,0DAA0D;IAC1D,YAAY,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,WAAW,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,eAAe,GAAG,qBAAqB,CA6BpF;AAED;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACjD;AAED,+FAA+F;AAC/F,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,WAAW,CAAC,EAAE,cAAc,CAAC;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC,eAAe,CAAC,CAgB1B"} | ||
| {"version":3,"file":"evidence.d.ts","sourceRoot":"","sources":["../src/evidence.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAI7C,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEvD,kFAAkF;AAClF,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,wBAAwB,CAAC;IACrC,WAAW,EAAE,CAAC,CAAC;IACf,OAAO,EAAE;QACP,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;QAC1B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,IAAI,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,SAAS,EAAE,UAAU,CAAC,WAAW,CAAC,CAAC;IACnC,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,0EAA0E;IAC1E,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,iBAAiB,CAAC;IACnC,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE;QACX,+EAA+E;QAC/E,WAAW,EAAE,MAAM,CAAC;QACpB;;;;WAIG;QACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB;;;WAGG;QACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,+EAA+E;QAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,6EAA6E;QAC7E,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,CAAC;CACH;AAeD,MAAM,WAAW,sBAAsB;IACrC,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB;;;;OAIG;IACH,OAAO,CAAC,EAAE,iBAAiB,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,UAAU,EAClB,IAAI,GAAE,sBAA2B,GAChC,eAAe,CAsDjB;AAED,mDAAmD;AACnD,MAAM,WAAW,qBAAqB;IACpC,oFAAoF;IACpF,KAAK,EAAE,OAAO,CAAC;IACf,0DAA0D;IAC1D,YAAY,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,WAAW,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,eAAe,GAAG,qBAAqB,CA8BpF;AAED;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACjD;AAED,+FAA+F;AAC/F,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,WAAW,CAAC,EAAE,cAAc,CAAC;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC,eAAe,CAAC,CAgB1B"} |
+11
-0
@@ -57,2 +57,11 @@ /** | ||
| const policyMapping = opts.policy ? buildPolicyMapping(result.findings, opts.policy) : undefined; | ||
| // Compliance mandates: attest the dated verdicts too. The evaluation is a pure | ||
| // function of (findings, date), and the findings are already hashed — so we | ||
| // DATE-PIN its `now` to a plain `YYYY-MM-DD` (dropping the volatile scan | ||
| // timestamp) and hash that. Two runs on the same commit ON THE SAME DAY then | ||
| // reproduce; a run after a deadline has passed correctly attests a different | ||
| // status (and a different hash). | ||
| const mandateMapping = opts.mandate | ||
| ? { ...opts.mandate, now: opts.mandate.now.slice(0, 10) } | ||
| : undefined; | ||
| const hashableBody = { | ||
@@ -70,2 +79,3 @@ reportType: "quantakrypto-readiness", | ||
| ...(policyMapping ? { policyMapping } : {}), | ||
| ...(mandateMapping ? { mandateMapping } : {}), | ||
| }; | ||
@@ -113,2 +123,3 @@ const contentHash = "sha256:" + | ||
| ...(report.policyMapping ? { policyMapping: report.policyMapping } : {}), | ||
| ...(report.mandateMapping ? { mandateMapping: report.mandateMapping } : {}), | ||
| }; | ||
@@ -115,0 +126,0 @@ const computedHash = "sha256:" + |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"evidence.js","sourceRoot":"","sources":["../src/evidence.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ISO/IEC 27001:2022 Annex A 8.24 evidence-chain export\n * (docs/compliance/iso27001-a8.24-evidence.md).\n *\n * Emits a self-describing readiness report bundling the scan result, crypto\n * inventory, and CycloneDX CBOM, plus an attestation carrying a DETERMINISTIC\n * content hash — the same scan over the same commit + config yields the same\n * hash (the volatile scan timestamp is deliberately excluded from the hashed\n * body). Signing + RFC-3161 timestamping are left to an EXTERNAL, vetted signer\n * (ADR-0004: this project performs no cryptography itself — it orchestrates a\n * signer, it does not implement one). SHA-256 here is an integrity hash (a Node\n * built-in), not an asymmetric primitive.\n *\n * Honesty boundary: this artifact is EVIDENCE for A.8.24, not the control. The\n * organization still owns the cryptography policy, key management, and the\n * conformance judgment. A clean scan is the absence of detected candidates, not\n * proof of quantum-safety (qScan is lexical). See docs/COMPLIANCE.md §3.\n */\nimport { createHash } from \"node:crypto\";\n\nimport type { ScanResult } from \"./types.js\";\nimport { toCbom } from \"./cbom.js\";\nimport { VERSION } from \"./version.js\";\nimport { buildPolicyMapping } from \"./policy.js\";\nimport type { CryptoPolicy, PolicyMapping } from \"./policy.js\";\n\n/** Stable per-finding record for the evidence body (deterministic per commit). */\nexport interface EvidenceFinding {\n ruleId: string;\n algorithm?: string;\n severity: string;\n hndl: boolean;\n file: string;\n line: number;\n}\n\nexport interface ReadinessReport {\n reportType: \"quantakrypto-readiness\";\n specVersion: 1;\n subject: {\n repository: string | null;\n commit: string | null;\n scannedRoot: string;\n scanTimeUtc: string;\n };\n tool: { name: \"qScan\"; version: string };\n inventory: ScanResult[\"inventory\"];\n findings: EvidenceFinding[];\n /** §4 policy verdicts, present only when a crypto policy was supplied. */\n policyMapping?: PolicyMapping;\n cbom: unknown;\n attestation: {\n /** sha256 over the canonicalized deterministic body (excludes scanTimeUtc). */\n contentHash: string;\n /**\n * RFC-3161 / transparency-log token over `contentHash`, produced by an EXTERNAL\n * timestamper (opaque string, e.g. base64). `null` until {@link signReadinessReport}\n * runs one.\n */\n timestamp: string | null;\n /**\n * Detached signature over `contentHash`, produced by an EXTERNAL signer (opaque\n * string, e.g. base64/PEM). `null` until {@link signReadinessReport} runs one.\n */\n signature: string | null;\n /** Non-sensitive provenance label of the signer (e.g. \"openssl\", \"cosign\"). */\n signedWith?: string;\n /** Non-sensitive provenance label of the timestamper (e.g. \"openssl-ts\"). */\n timestampedWith?: string;\n };\n}\n\n/** Canonical JSON: object keys sorted recursively, so the hash is reproducible. */\nfunction canonicalize(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(canonicalize);\n if (value && typeof value === \"object\") {\n const out: Record<string, unknown> = {};\n for (const k of Object.keys(value as Record<string, unknown>).sort()) {\n out[k] = canonicalize((value as Record<string, unknown>)[k]);\n }\n return out;\n }\n return value;\n}\n\nexport interface ReadinessReportOptions {\n /** Repository URL (e.g. from `GITHUB_REPOSITORY`); omitted → null. */\n repository?: string;\n /** Full commit SHA (e.g. from `GITHUB_SHA`); omitted → null. */\n commit?: string;\n /** Optional org cryptography policy — adds the §4 `policyMapping` verdicts. */\n policy?: CryptoPolicy;\n}\n\n/**\n * Build the A.8.24 readiness report for a scan result. The attestation's\n * `contentHash` covers everything EXCEPT the scan timestamp and the attestation\n * block itself, so re-running the same scan on the same commit is verifiable.\n */\nexport function buildReadinessReport(\n result: ScanResult,\n opts: ReadinessReportOptions = {},\n): ReadinessReport {\n const findings: EvidenceFinding[] = result.findings.map((f) => ({\n ruleId: f.ruleId,\n ...(f.algorithm ? { algorithm: f.algorithm } : {}),\n severity: f.severity,\n hndl: f.hndl,\n file: f.location.file,\n line: f.location.line,\n }));\n\n // The CBOM is a deterministic *view* of the (hashed) findings + inventory, but\n // its CycloneDX envelope carries a volatile timestamp/serial — so it is\n // EXCLUDED from the hashed body (its integrity follows from its hashed inputs)\n // to keep the content hash reproducible across scan runs on the same commit.\n // §4: if the org supplied a crypto policy, attest the per-finding verdicts too.\n // Deterministic (same findings + policy → same mapping), so it is hashed.\n const policyMapping = opts.policy ? buildPolicyMapping(result.findings, opts.policy) : undefined;\n\n const hashableBody = {\n reportType: \"quantakrypto-readiness\",\n specVersion: 1,\n subject: {\n repository: opts.repository ?? null,\n commit: opts.commit ?? null,\n scannedRoot: result.root,\n },\n tool: { name: \"qScan\", version: VERSION },\n inventory: result.inventory,\n findings,\n ...(policyMapping ? { policyMapping } : {}),\n };\n const contentHash =\n \"sha256:\" +\n createHash(\"sha256\")\n .update(JSON.stringify(canonicalize(hashableBody)))\n .digest(\"hex\");\n\n return {\n ...hashableBody,\n subject: { ...hashableBody.subject, scanTimeUtc: result.finishedAt },\n cbom: toCbom(result),\n attestation: { contentHash, timestamp: null, signature: null },\n } as ReadinessReport;\n}\n\n/** The result of {@link verifyReadinessReport}. */\nexport interface VerifyReadinessResult {\n /** True iff the recomputed body hash equals the hash claimed in the attestation. */\n valid: boolean;\n /** The hash recomputed over the report's CURRENT body. */\n computedHash: string;\n /** The hash claimed in the report's attestation (`attestation.contentHash`). */\n claimedHash: string;\n /** A short human reason; present only when `valid` is false. */\n reason?: string;\n}\n\n/**\n * Recompute the deterministic content hash over a readiness report's body and\n * compare it to the hash the attestation claims. Detects tampering with ANY\n * hashed field — a finding, the inventory, a policy verdict, or subject/tool\n * metadata: editing it after the fact changes the recomputed hash, so `valid`\n * becomes false.\n *\n * By construction the scan timestamp, the CBOM envelope, and the attestation\n * block itself are EXCLUDED from the hash (see {@link buildReadinessReport}), so\n * touching those does not fail verification — their integrity follows from their\n * hashed inputs. The body is reconstructed from the report's OWN stored fields\n * (including `tool.version`), so a report built by an older qScan still verifies.\n *\n * This checks the INTEGRITY hash only. It does NOT validate the detached\n * signature or RFC-3161 timestamp: those are opaque tokens from an external\n * signer (ADR-0004) and are verified with that signer's own tooling.\n */\nexport function verifyReadinessReport(report: ReadinessReport): VerifyReadinessResult {\n const hashableBody = {\n reportType: report.reportType,\n specVersion: report.specVersion,\n subject: {\n repository: report.subject.repository,\n commit: report.subject.commit,\n scannedRoot: report.subject.scannedRoot,\n },\n tool: { name: report.tool.name, version: report.tool.version },\n inventory: report.inventory,\n findings: report.findings,\n ...(report.policyMapping ? { policyMapping: report.policyMapping } : {}),\n };\n const computedHash =\n \"sha256:\" +\n createHash(\"sha256\")\n .update(JSON.stringify(canonicalize(hashableBody)))\n .digest(\"hex\");\n const claimedHash = report.attestation.contentHash;\n if (computedHash === claimedHash) {\n return { valid: true, computedHash, claimedHash };\n }\n return {\n valid: false,\n computedHash,\n claimedHash,\n reason: \"content-hash mismatch: the report body was modified after it was built\",\n };\n}\n\n/**\n * An EXTERNAL signer/timestamper the tool orchestrates. Per ADR-0004 the tool\n * implements no cryptography: it hands the payload to an operator-provided signer\n * (an `openssl`/`cosign` invocation, an RFC-3161 TSA client, …) and records what\n * comes back. `label` is a short, non-sensitive provenance string (e.g. the signer\n * program name) — NOT the full command, which may contain a key path.\n */\nexport interface EvidenceSigner {\n label: string;\n /**\n * Produce a detached signature / timestamp token (opaque string) over `payload`.\n * May be async so a future signer can shell out OR call a KMS / RFC-3161 TSA over\n * the network without foreclosing that once this contract freezes at 1.0.\n */\n sign(payload: string): string | Promise<string>;\n}\n\n/** Options for {@link signReadinessReport}: a detached-signature and/or a timestamp signer. */\nexport interface SignEvidenceOptions {\n signer?: EvidenceSigner;\n timestamper?: EvidenceSigner;\n}\n\n/**\n * Fill a readiness report's attestation with a detached signature and/or RFC-3161\n * timestamp, produced by EXTERNAL signers over the report's `contentHash`. Pure\n * orchestration: it invokes the injected signers and records their opaque output\n * plus a provenance label — it performs no cryptography itself (ADR-0004). Returns a\n * NEW report; the hashed body is untouched (attestation is excluded from the hash),\n * so signing never changes `contentHash`.\n */\nexport async function signReadinessReport(\n report: ReadinessReport,\n opts: SignEvidenceOptions,\n): Promise<ReadinessReport> {\n const payload = report.attestation.contentHash;\n const signature = opts.signer ? await opts.signer.sign(payload) : report.attestation.signature;\n const timestamp = opts.timestamper\n ? await opts.timestamper.sign(payload)\n : report.attestation.timestamp;\n return {\n ...report,\n attestation: {\n ...report.attestation,\n signature,\n timestamp,\n ...(opts.signer ? { signedWith: opts.signer.label } : {}),\n ...(opts.timestamper ? { timestampedWith: opts.timestamper.label } : {}),\n },\n };\n}\n"]} | ||
| {"version":3,"file":"evidence.js","sourceRoot":"","sources":["../src/evidence.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ISO/IEC 27001:2022 Annex A 8.24 evidence-chain export\n * (docs/compliance/iso27001-a8.24-evidence.md).\n *\n * Emits a self-describing readiness report bundling the scan result, crypto\n * inventory, and CycloneDX CBOM, plus an attestation carrying a DETERMINISTIC\n * content hash — the same scan over the same commit + config yields the same\n * hash (the volatile scan timestamp is deliberately excluded from the hashed\n * body). Signing + RFC-3161 timestamping are left to an EXTERNAL, vetted signer\n * (ADR-0004: this project performs no cryptography itself — it orchestrates a\n * signer, it does not implement one). SHA-256 here is an integrity hash (a Node\n * built-in), not an asymmetric primitive.\n *\n * Honesty boundary: this artifact is EVIDENCE for A.8.24, not the control. The\n * organization still owns the cryptography policy, key management, and the\n * conformance judgment. A clean scan is the absence of detected candidates, not\n * proof of quantum-safety (qScan is lexical). See docs/COMPLIANCE.md §3.\n */\nimport { createHash } from \"node:crypto\";\n\nimport type { ScanResult } from \"./types.js\";\nimport { toCbom } from \"./cbom.js\";\nimport { VERSION } from \"./version.js\";\nimport { buildPolicyMapping } from \"./policy.js\";\nimport type { CryptoPolicy, PolicyMapping } from \"./policy.js\";\nimport type { MandateEvaluation } from \"./mandates.js\";\n\n/** Stable per-finding record for the evidence body (deterministic per commit). */\nexport interface EvidenceFinding {\n ruleId: string;\n algorithm?: string;\n severity: string;\n hndl: boolean;\n file: string;\n line: number;\n}\n\nexport interface ReadinessReport {\n reportType: \"quantakrypto-readiness\";\n specVersion: 1;\n subject: {\n repository: string | null;\n commit: string | null;\n scannedRoot: string;\n scanTimeUtc: string;\n };\n tool: { name: \"qScan\"; version: string };\n inventory: ScanResult[\"inventory\"];\n findings: EvidenceFinding[];\n /** §4 policy verdicts, present only when a crypto policy was supplied. */\n policyMapping?: PolicyMapping;\n /**\n * Compliance-mandate verdicts (`--mandate`), present only when mandates were\n * evaluated. DATE-PINNED for reproducibility: its `now` is stored as a plain\n * `YYYY-MM-DD` (not the volatile scan timestamp) so the same scan of the same\n * commit ON THE SAME DAY yields the same attestation hash, while a genuinely\n * different compliance date (a passed deadline) correctly changes it.\n */\n mandateMapping?: MandateEvaluation;\n cbom: unknown;\n attestation: {\n /** sha256 over the canonicalized deterministic body (excludes scanTimeUtc). */\n contentHash: string;\n /**\n * RFC-3161 / transparency-log token over `contentHash`, produced by an EXTERNAL\n * timestamper (opaque string, e.g. base64). `null` until {@link signReadinessReport}\n * runs one.\n */\n timestamp: string | null;\n /**\n * Detached signature over `contentHash`, produced by an EXTERNAL signer (opaque\n * string, e.g. base64/PEM). `null` until {@link signReadinessReport} runs one.\n */\n signature: string | null;\n /** Non-sensitive provenance label of the signer (e.g. \"openssl\", \"cosign\"). */\n signedWith?: string;\n /** Non-sensitive provenance label of the timestamper (e.g. \"openssl-ts\"). */\n timestampedWith?: string;\n };\n}\n\n/** Canonical JSON: object keys sorted recursively, so the hash is reproducible. */\nfunction canonicalize(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(canonicalize);\n if (value && typeof value === \"object\") {\n const out: Record<string, unknown> = {};\n for (const k of Object.keys(value as Record<string, unknown>).sort()) {\n out[k] = canonicalize((value as Record<string, unknown>)[k]);\n }\n return out;\n }\n return value;\n}\n\nexport interface ReadinessReportOptions {\n /** Repository URL (e.g. from `GITHUB_REPOSITORY`); omitted → null. */\n repository?: string;\n /** Full commit SHA (e.g. from `GITHUB_SHA`); omitted → null. */\n commit?: string;\n /** Optional org cryptography policy — adds the §4 `policyMapping` verdicts. */\n policy?: CryptoPolicy;\n /**\n * Optional compliance-mandate evaluation ({@link evaluateMandates}) — adds the\n * `mandateMapping` block. Date-pinned into the hashed body (see\n * {@link ReadinessReport.mandateMapping}).\n */\n mandate?: MandateEvaluation;\n}\n\n/**\n * Build the A.8.24 readiness report for a scan result. The attestation's\n * `contentHash` covers everything EXCEPT the scan timestamp and the attestation\n * block itself, so re-running the same scan on the same commit is verifiable.\n */\nexport function buildReadinessReport(\n result: ScanResult,\n opts: ReadinessReportOptions = {},\n): ReadinessReport {\n const findings: EvidenceFinding[] = result.findings.map((f) => ({\n ruleId: f.ruleId,\n ...(f.algorithm ? { algorithm: f.algorithm } : {}),\n severity: f.severity,\n hndl: f.hndl,\n file: f.location.file,\n line: f.location.line,\n }));\n\n // The CBOM is a deterministic *view* of the (hashed) findings + inventory, but\n // its CycloneDX envelope carries a volatile timestamp/serial — so it is\n // EXCLUDED from the hashed body (its integrity follows from its hashed inputs)\n // to keep the content hash reproducible across scan runs on the same commit.\n // §4: if the org supplied a crypto policy, attest the per-finding verdicts too.\n // Deterministic (same findings + policy → same mapping), so it is hashed.\n const policyMapping = opts.policy ? buildPolicyMapping(result.findings, opts.policy) : undefined;\n\n // Compliance mandates: attest the dated verdicts too. The evaluation is a pure\n // function of (findings, date), and the findings are already hashed — so we\n // DATE-PIN its `now` to a plain `YYYY-MM-DD` (dropping the volatile scan\n // timestamp) and hash that. Two runs on the same commit ON THE SAME DAY then\n // reproduce; a run after a deadline has passed correctly attests a different\n // status (and a different hash).\n const mandateMapping = opts.mandate\n ? { ...opts.mandate, now: opts.mandate.now.slice(0, 10) }\n : undefined;\n\n const hashableBody = {\n reportType: \"quantakrypto-readiness\",\n specVersion: 1,\n subject: {\n repository: opts.repository ?? null,\n commit: opts.commit ?? null,\n scannedRoot: result.root,\n },\n tool: { name: \"qScan\", version: VERSION },\n inventory: result.inventory,\n findings,\n ...(policyMapping ? { policyMapping } : {}),\n ...(mandateMapping ? { mandateMapping } : {}),\n };\n const contentHash =\n \"sha256:\" +\n createHash(\"sha256\")\n .update(JSON.stringify(canonicalize(hashableBody)))\n .digest(\"hex\");\n\n return {\n ...hashableBody,\n subject: { ...hashableBody.subject, scanTimeUtc: result.finishedAt },\n cbom: toCbom(result),\n attestation: { contentHash, timestamp: null, signature: null },\n } as ReadinessReport;\n}\n\n/** The result of {@link verifyReadinessReport}. */\nexport interface VerifyReadinessResult {\n /** True iff the recomputed body hash equals the hash claimed in the attestation. */\n valid: boolean;\n /** The hash recomputed over the report's CURRENT body. */\n computedHash: string;\n /** The hash claimed in the report's attestation (`attestation.contentHash`). */\n claimedHash: string;\n /** A short human reason; present only when `valid` is false. */\n reason?: string;\n}\n\n/**\n * Recompute the deterministic content hash over a readiness report's body and\n * compare it to the hash the attestation claims. Detects tampering with ANY\n * hashed field — a finding, the inventory, a policy verdict, or subject/tool\n * metadata: editing it after the fact changes the recomputed hash, so `valid`\n * becomes false.\n *\n * By construction the scan timestamp, the CBOM envelope, and the attestation\n * block itself are EXCLUDED from the hash (see {@link buildReadinessReport}), so\n * touching those does not fail verification — their integrity follows from their\n * hashed inputs. The body is reconstructed from the report's OWN stored fields\n * (including `tool.version`), so a report built by an older qScan still verifies.\n *\n * This checks the INTEGRITY hash only. It does NOT validate the detached\n * signature or RFC-3161 timestamp: those are opaque tokens from an external\n * signer (ADR-0004) and are verified with that signer's own tooling.\n */\nexport function verifyReadinessReport(report: ReadinessReport): VerifyReadinessResult {\n const hashableBody = {\n reportType: report.reportType,\n specVersion: report.specVersion,\n subject: {\n repository: report.subject.repository,\n commit: report.subject.commit,\n scannedRoot: report.subject.scannedRoot,\n },\n tool: { name: report.tool.name, version: report.tool.version },\n inventory: report.inventory,\n findings: report.findings,\n ...(report.policyMapping ? { policyMapping: report.policyMapping } : {}),\n ...(report.mandateMapping ? { mandateMapping: report.mandateMapping } : {}),\n };\n const computedHash =\n \"sha256:\" +\n createHash(\"sha256\")\n .update(JSON.stringify(canonicalize(hashableBody)))\n .digest(\"hex\");\n const claimedHash = report.attestation.contentHash;\n if (computedHash === claimedHash) {\n return { valid: true, computedHash, claimedHash };\n }\n return {\n valid: false,\n computedHash,\n claimedHash,\n reason: \"content-hash mismatch: the report body was modified after it was built\",\n };\n}\n\n/**\n * An EXTERNAL signer/timestamper the tool orchestrates. Per ADR-0004 the tool\n * implements no cryptography: it hands the payload to an operator-provided signer\n * (an `openssl`/`cosign` invocation, an RFC-3161 TSA client, …) and records what\n * comes back. `label` is a short, non-sensitive provenance string (e.g. the signer\n * program name) — NOT the full command, which may contain a key path.\n */\nexport interface EvidenceSigner {\n label: string;\n /**\n * Produce a detached signature / timestamp token (opaque string) over `payload`.\n * May be async so a future signer can shell out OR call a KMS / RFC-3161 TSA over\n * the network without foreclosing that once this contract freezes at 1.0.\n */\n sign(payload: string): string | Promise<string>;\n}\n\n/** Options for {@link signReadinessReport}: a detached-signature and/or a timestamp signer. */\nexport interface SignEvidenceOptions {\n signer?: EvidenceSigner;\n timestamper?: EvidenceSigner;\n}\n\n/**\n * Fill a readiness report's attestation with a detached signature and/or RFC-3161\n * timestamp, produced by EXTERNAL signers over the report's `contentHash`. Pure\n * orchestration: it invokes the injected signers and records their opaque output\n * plus a provenance label — it performs no cryptography itself (ADR-0004). Returns a\n * NEW report; the hashed body is untouched (attestation is excluded from the hash),\n * so signing never changes `contentHash`.\n */\nexport async function signReadinessReport(\n report: ReadinessReport,\n opts: SignEvidenceOptions,\n): Promise<ReadinessReport> {\n const payload = report.attestation.contentHash;\n const signature = opts.signer ? await opts.signer.sign(payload) : report.attestation.signature;\n const timestamp = opts.timestamper\n ? await opts.timestamper.sign(payload)\n : report.attestation.timestamp;\n return {\n ...report,\n attestation: {\n ...report.attestation,\n signature,\n timestamp,\n ...(opts.signer ? { signedWith: opts.signer.label } : {}),\n ...(opts.timestamper ? { timestampedWith: opts.timestamper.label } : {}),\n },\n };\n}\n"]} |
+41
-2
@@ -6,3 +6,3 @@ /** | ||
| * date-blind. A mandate adds the missing dimension: named clauses with an effective | ||
| * DATE ("CNSA 2.0 disallows classical public-key crypto after 2035"). The evaluator | ||
| * DATE ("CNSA 2.0 disallows classical public-key crypto after 2033"). The evaluator | ||
| * compares each finding's algorithm against the selected mandates and today's date, | ||
@@ -24,2 +24,3 @@ * so a finding on a prohibited family reads as `due` (every deadline still ahead), | ||
| import type { AlgorithmFamily, Finding } from "./types.js"; | ||
| import type { CryptoPolicy, PolicyVerdict } from "./policy.js"; | ||
| /** Which enforcement tier a clause encodes: warn (`deprecate`) or fail (`disallow`). */ | ||
@@ -94,2 +95,17 @@ export type MandateRuleTier = "deprecate" | "disallow"; | ||
| citation: string; | ||
| /** | ||
| * The org cryptography policy's verdict on this algorithm family when a policy | ||
| * was composed in via {@link evaluateMandates}' `policy` argument, else null. | ||
| * Purely informational — it records the org's own stance next to the mandate's | ||
| * dated clause so a machine-readable report shows both. | ||
| */ | ||
| policyVerdict: PolicyVerdict | null; | ||
| /** | ||
| * True when the org policy EXPLICITLY permits or is transitioning this family — | ||
| * an owned, tracked decision. Acknowledged findings are exempt from the EARLY | ||
| * gates (`failNow` / `leadMonths`); a passed DISALLOW deadline (`violation`) | ||
| * still fails regardless, because an org cannot self-exempt from a dated legal | ||
| * disallow. `false` when no policy was supplied. | ||
| */ | ||
| acknowledged: boolean; | ||
| } | ||
@@ -118,2 +134,11 @@ export interface MandateEvaluation { | ||
| hasViolation: boolean; | ||
| /** Name of the org policy composed in via `policy`, or null when none was supplied. */ | ||
| policyName: string | null; | ||
| /** | ||
| * How many distinct prohibited FINDINGS the org policy explicitly acknowledged | ||
| * (family listed as `permitted` or `inTransition`) — counted per finding, not | ||
| * per verdict row, so a family prohibited by two mandates counts once, matching | ||
| * the per-finding `summary`. 0 when no policy was supplied. | ||
| */ | ||
| acknowledged: number; | ||
| } | ||
@@ -126,4 +151,11 @@ /** | ||
| * contributes a verdict row. | ||
| * | ||
| * When an org `policy` is supplied (the `--policy` composition), every verdict | ||
| * row is annotated with the org's own `policyVerdict` and an `acknowledged` flag | ||
| * (family explicitly permitted / in-transition). Acknowledgement is purely | ||
| * additive here — it changes no status — but {@link mandateGateFails} honours it | ||
| * to keep the early gates from double-flagging crypto the org is knowingly, | ||
| * traceably managing. A passed DISALLOW deadline is never acknowledgeable away. | ||
| */ | ||
| export declare function evaluateMandates(findings: readonly Finding[], mandateIdList: readonly string[], now: Date): MandateEvaluation; | ||
| export declare function evaluateMandates(findings: readonly Finding[], mandateIdList: readonly string[], now: Date, policy?: CryptoPolicy): MandateEvaluation; | ||
| export interface MandateGateOptions { | ||
@@ -141,4 +173,11 @@ /** Fail when a DISALLOW deadline is within this many months (early enforcement). */ | ||
| * immediately. | ||
| * | ||
| * Policy composition: when a finding was `acknowledged` by the org policy | ||
| * (`--policy`), it is exempt from the EARLY gates (`failNow` / `leadMonths`) — | ||
| * the org is knowingly, traceably managing that family, so its own early | ||
| * enforcement should not re-flag it. A passed DISALLOW deadline (`violation`) | ||
| * still fails regardless: a dated legal disallow is not something an org can | ||
| * self-exempt from. | ||
| */ | ||
| export declare function mandateGateFails(ev: MandateEvaluation, opts?: MandateGateOptions): boolean; | ||
| //# sourceMappingURL=mandates.d.ts.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"mandates.d.ts","sourceRoot":"","sources":["../src/mandates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAgD3D,wFAAwF;AACxF,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG,UAAU,CAAC;AAEvD,MAAM,WAAW,WAAW;IAC1B,mFAAmF;IACnF,MAAM,EAAE,MAAM,CAAC;IACf,yFAAyF;IACzF,IAAI,EAAE,eAAe,CAAC;IACtB,oEAAoE;IACpE,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,0DAA0D;IAC1D,SAAS,EAAE,MAAM,CAAC;IAClB,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB;AAED,8DAA8D;AAC9D,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CA+C5C,CAAC;AAEF,eAAO,MAAM,UAAU,QAAO,MAAM,EAA2B,CAAC;AAChE,eAAO,MAAM,UAAU,GAAI,IAAI,MAAM,KAAG,OAAO,GAAG,SAAyB,CAAC;AAE5E;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAOhE;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG,YAAY,GAAG,KAAK,GAAG,YAAY,GAAG,WAAW,CAAC;AAE9E,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,eAAe,GAAG,SAAS,CAAC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,oCAAoC;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,aAAa,CAAC;IACtB,sFAAsF;IACtF,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,+EAA+E;IAC/E,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,2DAA2D;IAC3D,GAAG,EAAE,MAAM,CAAC;IACZ,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;IACvC;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,QAAQ,EAAE,qBAAqB,EAAE,CAAC;IAClC,yEAAyE;IACzE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,kFAAkF;IAClF,YAAY,EAAE,OAAO,CAAC;CACvB;AAeD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,SAAS,OAAO,EAAE,EAC5B,aAAa,EAAE,SAAS,MAAM,EAAE,EAChC,GAAG,EAAE,IAAI,GACR,iBAAiB,CA0FnB;AAED,MAAM,WAAW,kBAAkB;IACjC,oFAAoF;IACpF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0EAA0E;IAC1E,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,iBAAiB,EAAE,IAAI,GAAE,kBAAuB,GAAG,OAAO,CAS9F"} | ||
| {"version":3,"file":"mandates.d.ts","sourceRoot":"","sources":["../src/mandates.ts"],"names":[],"mappings":"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"} |
+90
-23
| import { PQC_STANDARDS } from "./standards.js"; | ||
| import { verdictForAlgorithm } from "./policy.js"; | ||
| /** | ||
@@ -30,5 +31,8 @@ * All Shor-broken classical asymmetric families — the mandate's SCOPE. A finding | ||
| /** | ||
| * Effective dates derived from the standards source of truth | ||
| * (`PQC_STANDARDS.transitionTimeline`), so a quarterly standards update moves the | ||
| * mandate deadlines automatically (test/standards.test.ts asserts they agree). | ||
| * Effective dates derived from the standards source of truth, so a quarterly | ||
| * standards update moves the mandate deadlines automatically (test/standards.test.ts | ||
| * asserts they agree). Each regime uses its OWN dated timeline: NIST IR 8547 | ||
| * disallows after 2035, while CNSA 2.0 sets its general exclusive-use milestone | ||
| * at 2033 (both deprecate after 2030) — so the two mandates carry different | ||
| * disallow years rather than sharing one. | ||
| * | ||
@@ -40,5 +44,8 @@ * Boundary choice: "deprecate AFTER 2030" leaves the whole stated year permitted, | ||
| */ | ||
| const { deprecateAfter, disallowAfter } = PQC_STANDARDS.transitionTimeline; | ||
| const DEPRECATE_EFFECTIVE = `${deprecateAfter}-12-31`; | ||
| const DISALLOW_EFFECTIVE = `${disallowAfter}-12-31`; | ||
| const IR8547 = PQC_STANDARDS.transitionTimeline; // 2030 deprecate / 2035 disallow | ||
| const CNSA = PQC_STANDARDS.cnsaTimeline; // 2030 deprecate / 2033 disallow | ||
| const NIST_DEPRECATE_EFFECTIVE = `${IR8547.deprecateAfter}-12-31`; | ||
| const NIST_DISALLOW_EFFECTIVE = `${IR8547.disallowAfter}-12-31`; | ||
| const CNSA_DEPRECATE_EFFECTIVE = `${CNSA.deprecateAfter}-12-31`; | ||
| const CNSA_DISALLOW_EFFECTIVE = `${CNSA.disallowAfter}-12-31`; | ||
| /** The bundled mandate catalog. Keyed by `--mandate <id>`. */ | ||
@@ -54,14 +61,14 @@ export const MANDATES = { | ||
| { | ||
| clause: `CNSA 2.0 — deprecate classical PKC after ${deprecateAfter}`, | ||
| clause: `CNSA 2.0 — deprecate classical PKC after ${CNSA.deprecateAfter}`, | ||
| tier: "deprecate", | ||
| prohibits: [...PROHIBITED_FAMILIES], | ||
| effective: DEPRECATE_EFFECTIVE, | ||
| note: "Classical public-key cryptography deprecated; systems should use CNSA 2.0 PQC exclusively.", | ||
| effective: CNSA_DEPRECATE_EFFECTIVE, | ||
| note: `Classical public-key cryptography deprecated (${CNSA.deprecateAfter}: software/firmware signing exclusive-use); systems should use CNSA 2.0 PQC.`, | ||
| }, | ||
| { | ||
| clause: `CNSA 2.0 — disallow classical PKC after ${disallowAfter}`, | ||
| clause: `CNSA 2.0 — disallow classical PKC after ${CNSA.disallowAfter}`, | ||
| tier: "disallow", | ||
| prohibits: [...PROHIBITED_FAMILIES], | ||
| effective: DISALLOW_EFFECTIVE, | ||
| note: "Classical public-key cryptography disallowed; the migration must be complete.", | ||
| effective: CNSA_DISALLOW_EFFECTIVE, | ||
| note: `Classical public-key cryptography disallowed (${CNSA.disallowAfter}: general NSS exclusive-use milestone); the migration must be complete.`, | ||
| }, | ||
@@ -78,14 +85,14 @@ ], | ||
| { | ||
| clause: `NIST IR 8547 — deprecate classical PKC after ${deprecateAfter}`, | ||
| clause: `NIST IR 8547 — deprecate classical PKC after ${IR8547.deprecateAfter}`, | ||
| tier: "deprecate", | ||
| prohibits: [...PROHIBITED_FAMILIES], | ||
| effective: DEPRECATE_EFFECTIVE, | ||
| note: `112-bit-security classical public-key algorithms deprecated after ${deprecateAfter}.`, | ||
| effective: NIST_DEPRECATE_EFFECTIVE, | ||
| note: `112-bit-security classical public-key algorithms deprecated after ${IR8547.deprecateAfter}.`, | ||
| }, | ||
| { | ||
| clause: `NIST IR 8547 — disallow classical PKC after ${disallowAfter}`, | ||
| clause: `NIST IR 8547 — disallow classical PKC after ${IR8547.disallowAfter}`, | ||
| tier: "disallow", | ||
| prohibits: [...PROHIBITED_FAMILIES], | ||
| effective: DISALLOW_EFFECTIVE, | ||
| note: `Classical public-key algorithms disallowed after ${disallowAfter}.`, | ||
| effective: NIST_DISALLOW_EFFECTIVE, | ||
| note: `Classical public-key algorithms disallowed after ${IR8547.disallowAfter}.`, | ||
| }, | ||
@@ -121,2 +128,20 @@ ], | ||
| /** | ||
| * True when the org policy EXPLICITLY accepts a family — listed in `permitted` | ||
| * (an owned exception) or `inTransition` (a tracked migration). A `prohibited` | ||
| * family or one covered only by the policy's default fallback is NOT | ||
| * acknowledged: silence is not consent, so an unnamed family never earns a gate | ||
| * exemption. | ||
| * | ||
| * `prohibited` takes precedence, matching {@link verdictForAlgorithm}: a policy | ||
| * that lists a family in BOTH `prohibited` and `permitted` (a plausible merge of | ||
| * two policy fragments) resolves to `violation`, and must not then be silently | ||
| * acknowledged away — that would produce a self-contradictory verdict (verdict | ||
| * `violation`, yet exempt from the gate). | ||
| */ | ||
| function policyAcknowledges(algo, policy) { | ||
| if (policy.prohibited?.includes(algo)) | ||
| return false; | ||
| return Boolean(policy.permitted?.includes(algo) || policy.inTransition?.includes(algo)); | ||
| } | ||
| /** | ||
| * Evaluate findings against the selected mandates as of `now`. Unknown mandate | ||
@@ -127,5 +152,21 @@ * ids are ignored — callers validate up front with {@link assertKnownMandates}. | ||
| * contributes a verdict row. | ||
| * | ||
| * When an org `policy` is supplied (the `--policy` composition), every verdict | ||
| * row is annotated with the org's own `policyVerdict` and an `acknowledged` flag | ||
| * (family explicitly permitted / in-transition). Acknowledgement is purely | ||
| * additive here — it changes no status — but {@link mandateGateFails} honours it | ||
| * to keep the early gates from double-flagging crypto the org is knowingly, | ||
| * traceably managing. A passed DISALLOW deadline is never acknowledgeable away. | ||
| */ | ||
| export function evaluateMandates(findings, mandateIdList, now) { | ||
| const nowMs = now.getTime(); | ||
| export function evaluateMandates(findings, mandateIdList, now, policy) { | ||
| // A compliance verdict is as-of a DAY: the clauses take effect on date | ||
| // boundaries (YYYY-MM-DD), so the exact clock time carries no compliance | ||
| // meaning. Pin `now` to UTC midnight of its date before any arithmetic — this | ||
| // makes the whole evaluation (statuses AND the monthsUntil / monthsUntilDisallow | ||
| // counters) identical for any two runs on the same day, which is what keeps the | ||
| // attested evidence hash reproducible per commit per day. Truncating changes no | ||
| // status: every `effective` date is itself UTC-midnight, so `nowMs >= effMs` has | ||
| // the same truth value at midnight as at any other time that day. | ||
| const nowMs = Date.parse(`${now.toISOString().slice(0, 10)}T00:00:00.000Z`); | ||
| const nowIso = new Date(nowMs).toISOString(); | ||
| const selected = mandateIdList.map(getMandate).filter((m) => Boolean(m)); | ||
@@ -135,2 +176,3 @@ const rows = []; | ||
| let notInScope = 0; | ||
| let acknowledged = 0; | ||
| let nextDeadlineMs = null; | ||
@@ -144,2 +186,9 @@ for (const f of findings) { | ||
| let worst = "conformant"; | ||
| // Acknowledgement is a property of the FAMILY (fixed for this finding), so it | ||
| // is computed once here and stamped on every row. The tally counts distinct | ||
| // acknowledged findings (not rows), so one family under two mandates is one | ||
| // acknowledgement, matching the per-finding status counts in `summary`. | ||
| const family = algo; | ||
| const isAcknowledged = policy ? policyAcknowledges(family, policy) : false; | ||
| let producedRow = false; | ||
| for (const mandate of selected) { | ||
@@ -152,2 +201,3 @@ // The applicable clauses for this family, earliest deadline first. | ||
| continue; | ||
| producedRow = true; | ||
| // Tier the clauses so both stay live: a passed DISALLOW clause is a | ||
@@ -198,4 +248,10 @@ // violation; a passed DEPRECATE clause (disallow still ahead) is the | ||
| citation: mandate.citation, | ||
| policyVerdict: policy ? verdictForAlgorithm(family, policy).verdict : null, | ||
| acknowledged: isAcknowledged, | ||
| }); | ||
| } | ||
| // Count the acknowledged FINDING once (it produced at least one prohibited | ||
| // row and the org policy owns/tracks its family), not once per mandate row. | ||
| if (producedRow && isAcknowledged) | ||
| acknowledged++; | ||
| perFindingWorst.push(worst); | ||
@@ -212,3 +268,3 @@ } | ||
| return { | ||
| now: now.toISOString(), | ||
| now: nowIso, | ||
| mandates: selected.map((m) => m.id), | ||
@@ -220,2 +276,4 @@ summary, | ||
| hasViolation: summary.violation > 0, | ||
| policyName: policy?.name ?? null, | ||
| acknowledged, | ||
| }; | ||
@@ -229,2 +287,9 @@ } | ||
| * immediately. | ||
| * | ||
| * Policy composition: when a finding was `acknowledged` by the org policy | ||
| * (`--policy`), it is exempt from the EARLY gates (`failNow` / `leadMonths`) — | ||
| * the org is knowingly, traceably managing that family, so its own early | ||
| * enforcement should not re-flag it. A passed DISALLOW deadline (`violation`) | ||
| * still fails regardless: a dated legal disallow is not something an org can | ||
| * self-exempt from. | ||
| */ | ||
@@ -234,6 +299,8 @@ export function mandateGateFails(ev, opts = {}) { | ||
| return true; | ||
| // Early gates skip policy-acknowledged findings; the hard `violation` above did not. | ||
| const gated = ev.findings.filter((v) => !v.acknowledged); | ||
| if (opts.failNow) | ||
| return ev.findings.length > 0; | ||
| return gated.length > 0; | ||
| if (opts.leadMonths !== undefined) { | ||
| return ev.findings.some((v) => v.monthsUntilDisallow !== null && v.monthsUntilDisallow <= opts.leadMonths); | ||
| return gated.some((v) => v.monthsUntilDisallow !== null && v.monthsUntilDisallow <= opts.leadMonths); | ||
| } | ||
@@ -240,0 +307,0 @@ return false; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"mandates.js","sourceRoot":"","sources":["../src/mandates.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Policy-as-code compliance mandates → dated, clause-named verdicts for findings.\n *\n * `CryptoPolicy` (policy.ts) classifies findings by algorithm family but is\n * date-blind. A mandate adds the missing dimension: named clauses with an effective\n * DATE (\"CNSA 2.0 disallows classical public-key crypto after 2035\"). The evaluator\n * compares each finding's algorithm against the selected mandates and today's date,\n * so a finding on a prohibited family reads as `due` (every deadline still ahead),\n * `deprecated` (the DEPRECATE deadline has passed — a warning), or `violation` (the\n * DISALLOW deadline has passed — a failure), always naming the governing clause,\n * deadline, and citation. This is what turns the inventory into an enforceable,\n * mandate-mapped gate rather than a neutral list.\n *\n * Pure and deterministic (the caller supplies `now`), so it is trivially testable.\n * qScan consumes it today for the `--mandate` gate; because it operates on the\n * shared `Finding[]`, qProbe or the GitHub Action can reuse it unchanged.\n *\n * Catalog scope: the two regimes that carry hard algorithm deadlines — CNSA 2.0 and\n * NIST IR 8547. DORA / NIS2 / PCI DSS require approved cryptography but set no\n * independent algorithm date; they inherit these timelines and are cited in docs.\n */\nimport type { AlgorithmFamily, Finding } from \"./types.js\";\nimport { PQC_STANDARDS } from \"./standards.js\";\n\n/**\n * All Shor-broken classical asymmetric families — the mandate's SCOPE. A finding\n * on one of these is adjudicated against the selected mandates; findings on\n * anything else (hashes, RNG, dependency, or TLS-configuration findings) are out\n * of scope for a PQC-asymmetric mandate and are tallied as `notInScope` instead\n * of inflating the conformant count.\n */\nconst CLASSICAL_PUBLIC_KEY: readonly AlgorithmFamily[] = [\n \"RSA\",\n \"ECDH\",\n \"ECDSA\",\n \"EdDSA\",\n \"DH\",\n \"DSA\",\n \"X25519\",\n \"X448\",\n \"ECIES\",\n];\n\n/**\n * The PROHIBITED subset the dated clauses apply to. X25519 and X448 are\n * deliberately excluded: they are the classical half of the recommended hybrid\n * key exchange (X25519MLKEM768 — permitted and recommended under the NIST\n * profile), and a static scan cannot distinguish a standalone exchange from the\n * hybrid's classical leg. Prohibiting them would false-positive exactly the orgs\n * that hybridized correctly, so they stay in scope but read `conformant`.\n */\nconst PROHIBITED_FAMILIES: readonly AlgorithmFamily[] = CLASSICAL_PUBLIC_KEY.filter(\n (family) => family !== \"X25519\" && family !== \"X448\",\n);\n\n/**\n * Effective dates derived from the standards source of truth\n * (`PQC_STANDARDS.transitionTimeline`), so a quarterly standards update moves the\n * mandate deadlines automatically (test/standards.test.ts asserts they agree).\n *\n * Boundary choice: \"deprecate AFTER 2030\" leaves the whole stated year permitted,\n * so each clause takes effect on the LAST day of its year (`YYYY-12-31`) —\n * conservative by a single day, unlike `YYYY-01-01`, which would bite roughly a\n * year early.\n */\nconst { deprecateAfter, disallowAfter } = PQC_STANDARDS.transitionTimeline;\nconst DEPRECATE_EFFECTIVE = `${deprecateAfter}-12-31`;\nconst DISALLOW_EFFECTIVE = `${disallowAfter}-12-31`;\n\n/** Which enforcement tier a clause encodes: warn (`deprecate`) or fail (`disallow`). */\nexport type MandateRuleTier = \"deprecate\" | \"disallow\";\n\nexport interface MandateRule {\n /** The named clause this rule encodes (verbatim in the gate's failure message). */\n clause: string;\n /** Enforcement tier: a passed `deprecate` date warns; a passed `disallow` date fails. */\n tier: MandateRuleTier;\n /** Algorithm families prohibited from the effective date onward. */\n prohibits: AlgorithmFamily[];\n /** ISO date (YYYY-MM-DD) the prohibition takes effect. */\n effective: string;\n /** One-line human description of the clause. */\n note: string;\n}\n\nexport interface Mandate {\n id: string;\n name: string;\n authority: string;\n citation: string;\n /** When this catalog entry was last reviewed against the source. */\n asOf: string;\n rules: MandateRule[];\n}\n\n/** The bundled mandate catalog. Keyed by `--mandate <id>`. */\nexport const MANDATES: Record<string, Mandate> = {\n \"cnsa-2.0\": {\n id: \"cnsa-2.0\",\n name: \"CNSA 2.0\",\n authority: \"NSA\",\n citation: \"NSA Commercial National Security Algorithm Suite 2.0 (CNSA 2.0)\",\n asOf: \"2026-07\",\n rules: [\n {\n clause: `CNSA 2.0 — deprecate classical PKC after ${deprecateAfter}`,\n tier: \"deprecate\",\n prohibits: [...PROHIBITED_FAMILIES],\n effective: DEPRECATE_EFFECTIVE,\n note: \"Classical public-key cryptography deprecated; systems should use CNSA 2.0 PQC exclusively.\",\n },\n {\n clause: `CNSA 2.0 — disallow classical PKC after ${disallowAfter}`,\n tier: \"disallow\",\n prohibits: [...PROHIBITED_FAMILIES],\n effective: DISALLOW_EFFECTIVE,\n note: \"Classical public-key cryptography disallowed; the migration must be complete.\",\n },\n ],\n },\n \"nist-ir-8547\": {\n id: \"nist-ir-8547\",\n name: \"NIST IR 8547\",\n authority: \"NIST\",\n citation: \"NIST IR 8547 (Transition to Post-Quantum Cryptography Standards)\",\n asOf: \"2026-07\",\n rules: [\n {\n clause: `NIST IR 8547 — deprecate classical PKC after ${deprecateAfter}`,\n tier: \"deprecate\",\n prohibits: [...PROHIBITED_FAMILIES],\n effective: DEPRECATE_EFFECTIVE,\n note: `112-bit-security classical public-key algorithms deprecated after ${deprecateAfter}.`,\n },\n {\n clause: `NIST IR 8547 — disallow classical PKC after ${disallowAfter}`,\n tier: \"disallow\",\n prohibits: [...PROHIBITED_FAMILIES],\n effective: DISALLOW_EFFECTIVE,\n note: `Classical public-key algorithms disallowed after ${disallowAfter}.`,\n },\n ],\n },\n};\n\nexport const mandateIds = (): string[] => Object.keys(MANDATES);\nexport const getMandate = (id: string): Mandate | undefined => MANDATES[id];\n\n/**\n * Validate mandate ids loudly, matching `parseCryptoPolicy`'s fail-loud\n * convention: a mistyped id must never silently evaluate to an empty gate.\n * Throws an `Error` naming every unknown id and the known catalog.\n * `evaluateMandates` itself stays lenient (unknown ids are skipped) so callers\n * decide where to fail.\n */\nexport function assertKnownMandates(ids: readonly string[]): void {\n const unknown = ids.filter((id) => !MANDATES[id]);\n if (unknown.length > 0) {\n throw new Error(\n `unknown mandate id(s): ${unknown.join(\", \")}; known mandates: ${mandateIds().join(\", \")}`,\n );\n }\n}\n\n/**\n * A finding's status against a mandate, worst last:\n * - `conformant` — in scope (classical asymmetric) but prohibited by no selected\n * clause (e.g. X25519 as the presumed hybrid leg).\n * - `due` — prohibited, with every deadline still ahead.\n * - `deprecated` — the DEPRECATE deadline has passed; a warning, not a failure.\n * - `violation` — the DISALLOW deadline has passed; fails the default gate.\n */\nexport type MandateStatus = \"conformant\" | \"due\" | \"deprecated\" | \"violation\";\n\nexport interface MandateFindingVerdict {\n ruleId: string;\n algorithm: AlgorithmFamily | \"unknown\";\n file: string;\n line: number;\n /** Mandate id (e.g. \"cnsa-2.0\"). */\n mandate: string;\n /**\n * The governing clause: the next upcoming clause when `due`, the passed\n * DEPRECATE clause when `deprecated`, the passed DISALLOW clause on `violation`.\n */\n clause: string;\n /** ISO effective date of the governing clause. */\n effective: string;\n status: MandateStatus;\n /** Whole months from `now` to the governing deadline; negative once it has passed. */\n monthsUntil: number;\n /**\n * ISO effective date of this mandate's DISALLOW clause for the family, or null\n * when the mandate carries none. The gate's `leadMonths` measures against this.\n */\n disallowEffective: string | null;\n /** Whole months from `now` to `disallowEffective`; null when there is none. */\n monthsUntilDisallow: number | null;\n citation: string;\n}\n\nexport interface MandateEvaluation {\n /** The `now` the evaluation was computed against (ISO). */\n now: string;\n /** Mandate ids evaluated. */\n mandates: string[];\n /**\n * Counts of IN-SCOPE findings (classical asymmetric families) by their worst\n * status across the selected mandates. Out-of-scope findings are excluded so\n * the conformant count is an honest statement about asymmetric crypto only.\n */\n summary: Record<MandateStatus, number>;\n /**\n * Findings outside the mandate's scope (hashes, RNG, dependency, TLS-config…),\n * which a PQC-asymmetric mandate does not adjudicate.\n */\n notInScope: number;\n /** One row per (prohibited finding × applicable mandate). */\n findings: MandateFindingVerdict[];\n /** Earliest still-future deadline across non-violation rows, or null. */\n nextDeadline: string | null;\n /** True when at least one DISALLOW deadline has passed (a `violation` exists). */\n hasViolation: boolean;\n}\n\nconst MONTH_MS = 2_629_800_000; // average month\n\nfunction monthsBetween(fromMs: number, toMs: number): number {\n return Math.round((toMs - fromMs) / MONTH_MS);\n}\n\nconst STATUS_RANK: Record<MandateStatus, number> = {\n conformant: 0,\n due: 1,\n deprecated: 2,\n violation: 3,\n};\n\n/**\n * Evaluate findings against the selected mandates as of `now`. Unknown mandate\n * ids are ignored — callers validate up front with {@link assertKnownMandates}.\n * A finding outside the classical-asymmetric scope is counted in `notInScope`;\n * an in-scope finding no selected mandate prohibits is `conformant`. Neither\n * contributes a verdict row.\n */\nexport function evaluateMandates(\n findings: readonly Finding[],\n mandateIdList: readonly string[],\n now: Date,\n): MandateEvaluation {\n const nowMs = now.getTime();\n const selected = mandateIdList.map(getMandate).filter((m): m is Mandate => Boolean(m));\n\n const rows: MandateFindingVerdict[] = [];\n const perFindingWorst: MandateStatus[] = [];\n let notInScope = 0;\n let nextDeadlineMs: number | null = null;\n\n for (const f of findings) {\n const algo = f.algorithm ?? \"unknown\";\n if (!CLASSICAL_PUBLIC_KEY.includes(algo as AlgorithmFamily)) {\n notInScope++;\n continue;\n }\n let worst: MandateStatus = \"conformant\";\n for (const mandate of selected) {\n // The applicable clauses for this family, earliest deadline first.\n const applicable = mandate.rules\n .filter((r) => r.prohibits.includes(algo as AlgorithmFamily))\n .sort((a, b) => a.effective.localeCompare(b.effective));\n if (applicable.length === 0) continue;\n\n // Tier the clauses so both stay live: a passed DISALLOW clause is a\n // violation; a passed DEPRECATE clause (disallow still ahead) is the\n // deprecated warning tier; otherwise the finding is due against the next\n // upcoming clause.\n const passed = applicable.filter((r) => nowMs >= new Date(r.effective).getTime());\n const passedDisallow = passed.filter((r) => r.tier === \"disallow\");\n let status: MandateStatus;\n let governing: MandateRule;\n if (passedDisallow.length > 0) {\n status = \"violation\";\n governing = passedDisallow[0];\n } else if (passed.length > 0) {\n status = \"deprecated\";\n governing = passed[passed.length - 1];\n } else {\n status = \"due\";\n governing = applicable[0];\n }\n\n // The disallow clause (earliest, if several) anchors `leadMonths`.\n const disallowRule = applicable.find((r) => r.tier === \"disallow\") ?? null;\n const disallowMs = disallowRule ? new Date(disallowRule.effective).getTime() : null;\n\n if (status !== \"violation\") {\n for (const r of applicable) {\n const effMs = new Date(r.effective).getTime();\n if (effMs > nowMs && (nextDeadlineMs === null || effMs < nextDeadlineMs))\n nextDeadlineMs = effMs;\n }\n }\n if (STATUS_RANK[status] > STATUS_RANK[worst]) worst = status;\n rows.push({\n ruleId: f.ruleId,\n algorithm: algo,\n file: f.location.file,\n line: f.location.line,\n mandate: mandate.id,\n clause: governing.clause,\n effective: governing.effective,\n status,\n monthsUntil: monthsBetween(nowMs, new Date(governing.effective).getTime()),\n disallowEffective: disallowRule ? disallowRule.effective : null,\n monthsUntilDisallow: disallowMs !== null ? monthsBetween(nowMs, disallowMs) : null,\n citation: mandate.citation,\n });\n }\n perFindingWorst.push(worst);\n }\n\n const summary: Record<MandateStatus, number> = {\n conformant: 0,\n due: 0,\n deprecated: 0,\n violation: 0,\n };\n for (const s of perFindingWorst) summary[s]++;\n\n return {\n now: now.toISOString(),\n mandates: selected.map((m) => m.id),\n summary,\n notInScope,\n findings: rows,\n nextDeadline:\n nextDeadlineMs !== null ? new Date(nextDeadlineMs).toISOString().slice(0, 10) : null,\n hasViolation: summary.violation > 0,\n };\n}\n\nexport interface MandateGateOptions {\n /** Fail when a DISALLOW deadline is within this many months (early enforcement). */\n leadMonths?: number;\n /** Fail on any mandate-prohibited finding regardless of the deadlines. */\n failNow?: boolean;\n}\n\n/**\n * The gate decision under the \"deadline-aware\" default: fail only once a DISALLOW\n * deadline has passed (`violation`). A passed DEPRECATE date (`deprecated`) is a\n * warning and does not fail the build. `leadMonths` fails early when a disallow\n * deadline is within the window; `failNow` fails on any prohibited finding\n * immediately.\n */\nexport function mandateGateFails(ev: MandateEvaluation, opts: MandateGateOptions = {}): boolean {\n if (ev.hasViolation) return true;\n if (opts.failNow) return ev.findings.length > 0;\n if (opts.leadMonths !== undefined) {\n return ev.findings.some(\n (v) => v.monthsUntilDisallow !== null && v.monthsUntilDisallow <= opts.leadMonths!,\n );\n }\n return false;\n}\n"]} | ||
| {"version":3,"file":"mandates.js","sourceRoot":"","sources":["../src/mandates.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Policy-as-code compliance mandates → dated, clause-named verdicts for findings.\n *\n * `CryptoPolicy` (policy.ts) classifies findings by algorithm family but is\n * date-blind. A mandate adds the missing dimension: named clauses with an effective\n * DATE (\"CNSA 2.0 disallows classical public-key crypto after 2033\"). The evaluator\n * compares each finding's algorithm against the selected mandates and today's date,\n * so a finding on a prohibited family reads as `due` (every deadline still ahead),\n * `deprecated` (the DEPRECATE deadline has passed — a warning), or `violation` (the\n * DISALLOW deadline has passed — a failure), always naming the governing clause,\n * deadline, and citation. This is what turns the inventory into an enforceable,\n * mandate-mapped gate rather than a neutral list.\n *\n * Pure and deterministic (the caller supplies `now`), so it is trivially testable.\n * qScan consumes it today for the `--mandate` gate; because it operates on the\n * shared `Finding[]`, qProbe or the GitHub Action can reuse it unchanged.\n *\n * Catalog scope: the two regimes that carry hard algorithm deadlines — CNSA 2.0 and\n * NIST IR 8547. DORA / NIS2 / PCI DSS require approved cryptography but set no\n * independent algorithm date; they inherit these timelines and are cited in docs.\n */\nimport type { AlgorithmFamily, Finding } from \"./types.js\";\nimport { PQC_STANDARDS } from \"./standards.js\";\nimport { verdictForAlgorithm } from \"./policy.js\";\nimport type { CryptoPolicy, PolicyVerdict } from \"./policy.js\";\n\n/**\n * All Shor-broken classical asymmetric families — the mandate's SCOPE. A finding\n * on one of these is adjudicated against the selected mandates; findings on\n * anything else (hashes, RNG, dependency, or TLS-configuration findings) are out\n * of scope for a PQC-asymmetric mandate and are tallied as `notInScope` instead\n * of inflating the conformant count.\n */\nconst CLASSICAL_PUBLIC_KEY: readonly AlgorithmFamily[] = [\n \"RSA\",\n \"ECDH\",\n \"ECDSA\",\n \"EdDSA\",\n \"DH\",\n \"DSA\",\n \"X25519\",\n \"X448\",\n \"ECIES\",\n];\n\n/**\n * The PROHIBITED subset the dated clauses apply to. X25519 and X448 are\n * deliberately excluded: they are the classical half of the recommended hybrid\n * key exchange (X25519MLKEM768 — permitted and recommended under the NIST\n * profile), and a static scan cannot distinguish a standalone exchange from the\n * hybrid's classical leg. Prohibiting them would false-positive exactly the orgs\n * that hybridized correctly, so they stay in scope but read `conformant`.\n */\nconst PROHIBITED_FAMILIES: readonly AlgorithmFamily[] = CLASSICAL_PUBLIC_KEY.filter(\n (family) => family !== \"X25519\" && family !== \"X448\",\n);\n\n/**\n * Effective dates derived from the standards source of truth, so a quarterly\n * standards update moves the mandate deadlines automatically (test/standards.test.ts\n * asserts they agree). Each regime uses its OWN dated timeline: NIST IR 8547\n * disallows after 2035, while CNSA 2.0 sets its general exclusive-use milestone\n * at 2033 (both deprecate after 2030) — so the two mandates carry different\n * disallow years rather than sharing one.\n *\n * Boundary choice: \"deprecate AFTER 2030\" leaves the whole stated year permitted,\n * so each clause takes effect on the LAST day of its year (`YYYY-12-31`) —\n * conservative by a single day, unlike `YYYY-01-01`, which would bite roughly a\n * year early.\n */\nconst IR8547 = PQC_STANDARDS.transitionTimeline; // 2030 deprecate / 2035 disallow\nconst CNSA = PQC_STANDARDS.cnsaTimeline; // 2030 deprecate / 2033 disallow\nconst NIST_DEPRECATE_EFFECTIVE = `${IR8547.deprecateAfter}-12-31`;\nconst NIST_DISALLOW_EFFECTIVE = `${IR8547.disallowAfter}-12-31`;\nconst CNSA_DEPRECATE_EFFECTIVE = `${CNSA.deprecateAfter}-12-31`;\nconst CNSA_DISALLOW_EFFECTIVE = `${CNSA.disallowAfter}-12-31`;\n\n/** Which enforcement tier a clause encodes: warn (`deprecate`) or fail (`disallow`). */\nexport type MandateRuleTier = \"deprecate\" | \"disallow\";\n\nexport interface MandateRule {\n /** The named clause this rule encodes (verbatim in the gate's failure message). */\n clause: string;\n /** Enforcement tier: a passed `deprecate` date warns; a passed `disallow` date fails. */\n tier: MandateRuleTier;\n /** Algorithm families prohibited from the effective date onward. */\n prohibits: AlgorithmFamily[];\n /** ISO date (YYYY-MM-DD) the prohibition takes effect. */\n effective: string;\n /** One-line human description of the clause. */\n note: string;\n}\n\nexport interface Mandate {\n id: string;\n name: string;\n authority: string;\n citation: string;\n /** When this catalog entry was last reviewed against the source. */\n asOf: string;\n rules: MandateRule[];\n}\n\n/** The bundled mandate catalog. Keyed by `--mandate <id>`. */\nexport const MANDATES: Record<string, Mandate> = {\n \"cnsa-2.0\": {\n id: \"cnsa-2.0\",\n name: \"CNSA 2.0\",\n authority: \"NSA\",\n citation: \"NSA Commercial National Security Algorithm Suite 2.0 (CNSA 2.0)\",\n asOf: \"2026-07\",\n rules: [\n {\n clause: `CNSA 2.0 — deprecate classical PKC after ${CNSA.deprecateAfter}`,\n tier: \"deprecate\",\n prohibits: [...PROHIBITED_FAMILIES],\n effective: CNSA_DEPRECATE_EFFECTIVE,\n note: `Classical public-key cryptography deprecated (${CNSA.deprecateAfter}: software/firmware signing exclusive-use); systems should use CNSA 2.0 PQC.`,\n },\n {\n clause: `CNSA 2.0 — disallow classical PKC after ${CNSA.disallowAfter}`,\n tier: \"disallow\",\n prohibits: [...PROHIBITED_FAMILIES],\n effective: CNSA_DISALLOW_EFFECTIVE,\n note: `Classical public-key cryptography disallowed (${CNSA.disallowAfter}: general NSS exclusive-use milestone); the migration must be complete.`,\n },\n ],\n },\n \"nist-ir-8547\": {\n id: \"nist-ir-8547\",\n name: \"NIST IR 8547\",\n authority: \"NIST\",\n citation: \"NIST IR 8547 (Transition to Post-Quantum Cryptography Standards)\",\n asOf: \"2026-07\",\n rules: [\n {\n clause: `NIST IR 8547 — deprecate classical PKC after ${IR8547.deprecateAfter}`,\n tier: \"deprecate\",\n prohibits: [...PROHIBITED_FAMILIES],\n effective: NIST_DEPRECATE_EFFECTIVE,\n note: `112-bit-security classical public-key algorithms deprecated after ${IR8547.deprecateAfter}.`,\n },\n {\n clause: `NIST IR 8547 — disallow classical PKC after ${IR8547.disallowAfter}`,\n tier: \"disallow\",\n prohibits: [...PROHIBITED_FAMILIES],\n effective: NIST_DISALLOW_EFFECTIVE,\n note: `Classical public-key algorithms disallowed after ${IR8547.disallowAfter}.`,\n },\n ],\n },\n};\n\nexport const mandateIds = (): string[] => Object.keys(MANDATES);\nexport const getMandate = (id: string): Mandate | undefined => MANDATES[id];\n\n/**\n * Validate mandate ids loudly, matching `parseCryptoPolicy`'s fail-loud\n * convention: a mistyped id must never silently evaluate to an empty gate.\n * Throws an `Error` naming every unknown id and the known catalog.\n * `evaluateMandates` itself stays lenient (unknown ids are skipped) so callers\n * decide where to fail.\n */\nexport function assertKnownMandates(ids: readonly string[]): void {\n const unknown = ids.filter((id) => !MANDATES[id]);\n if (unknown.length > 0) {\n throw new Error(\n `unknown mandate id(s): ${unknown.join(\", \")}; known mandates: ${mandateIds().join(\", \")}`,\n );\n }\n}\n\n/**\n * A finding's status against a mandate, worst last:\n * - `conformant` — in scope (classical asymmetric) but prohibited by no selected\n * clause (e.g. X25519 as the presumed hybrid leg).\n * - `due` — prohibited, with every deadline still ahead.\n * - `deprecated` — the DEPRECATE deadline has passed; a warning, not a failure.\n * - `violation` — the DISALLOW deadline has passed; fails the default gate.\n */\nexport type MandateStatus = \"conformant\" | \"due\" | \"deprecated\" | \"violation\";\n\nexport interface MandateFindingVerdict {\n ruleId: string;\n algorithm: AlgorithmFamily | \"unknown\";\n file: string;\n line: number;\n /** Mandate id (e.g. \"cnsa-2.0\"). */\n mandate: string;\n /**\n * The governing clause: the next upcoming clause when `due`, the passed\n * DEPRECATE clause when `deprecated`, the passed DISALLOW clause on `violation`.\n */\n clause: string;\n /** ISO effective date of the governing clause. */\n effective: string;\n status: MandateStatus;\n /** Whole months from `now` to the governing deadline; negative once it has passed. */\n monthsUntil: number;\n /**\n * ISO effective date of this mandate's DISALLOW clause for the family, or null\n * when the mandate carries none. The gate's `leadMonths` measures against this.\n */\n disallowEffective: string | null;\n /** Whole months from `now` to `disallowEffective`; null when there is none. */\n monthsUntilDisallow: number | null;\n citation: string;\n /**\n * The org cryptography policy's verdict on this algorithm family when a policy\n * was composed in via {@link evaluateMandates}' `policy` argument, else null.\n * Purely informational — it records the org's own stance next to the mandate's\n * dated clause so a machine-readable report shows both.\n */\n policyVerdict: PolicyVerdict | null;\n /**\n * True when the org policy EXPLICITLY permits or is transitioning this family —\n * an owned, tracked decision. Acknowledged findings are exempt from the EARLY\n * gates (`failNow` / `leadMonths`); a passed DISALLOW deadline (`violation`)\n * still fails regardless, because an org cannot self-exempt from a dated legal\n * disallow. `false` when no policy was supplied.\n */\n acknowledged: boolean;\n}\n\nexport interface MandateEvaluation {\n /** The `now` the evaluation was computed against (ISO). */\n now: string;\n /** Mandate ids evaluated. */\n mandates: string[];\n /**\n * Counts of IN-SCOPE findings (classical asymmetric families) by their worst\n * status across the selected mandates. Out-of-scope findings are excluded so\n * the conformant count is an honest statement about asymmetric crypto only.\n */\n summary: Record<MandateStatus, number>;\n /**\n * Findings outside the mandate's scope (hashes, RNG, dependency, TLS-config…),\n * which a PQC-asymmetric mandate does not adjudicate.\n */\n notInScope: number;\n /** One row per (prohibited finding × applicable mandate). */\n findings: MandateFindingVerdict[];\n /** Earliest still-future deadline across non-violation rows, or null. */\n nextDeadline: string | null;\n /** True when at least one DISALLOW deadline has passed (a `violation` exists). */\n hasViolation: boolean;\n /** Name of the org policy composed in via `policy`, or null when none was supplied. */\n policyName: string | null;\n /**\n * How many distinct prohibited FINDINGS the org policy explicitly acknowledged\n * (family listed as `permitted` or `inTransition`) — counted per finding, not\n * per verdict row, so a family prohibited by two mandates counts once, matching\n * the per-finding `summary`. 0 when no policy was supplied.\n */\n acknowledged: number;\n}\n\nconst MONTH_MS = 2_629_800_000; // average month\n\nfunction monthsBetween(fromMs: number, toMs: number): number {\n return Math.round((toMs - fromMs) / MONTH_MS);\n}\n\nconst STATUS_RANK: Record<MandateStatus, number> = {\n conformant: 0,\n due: 1,\n deprecated: 2,\n violation: 3,\n};\n\n/**\n * True when the org policy EXPLICITLY accepts a family — listed in `permitted`\n * (an owned exception) or `inTransition` (a tracked migration). A `prohibited`\n * family or one covered only by the policy's default fallback is NOT\n * acknowledged: silence is not consent, so an unnamed family never earns a gate\n * exemption.\n *\n * `prohibited` takes precedence, matching {@link verdictForAlgorithm}: a policy\n * that lists a family in BOTH `prohibited` and `permitted` (a plausible merge of\n * two policy fragments) resolves to `violation`, and must not then be silently\n * acknowledged away — that would produce a self-contradictory verdict (verdict\n * `violation`, yet exempt from the gate).\n */\nfunction policyAcknowledges(algo: AlgorithmFamily, policy: CryptoPolicy): boolean {\n if (policy.prohibited?.includes(algo)) return false;\n return Boolean(policy.permitted?.includes(algo) || policy.inTransition?.includes(algo));\n}\n\n/**\n * Evaluate findings against the selected mandates as of `now`. Unknown mandate\n * ids are ignored — callers validate up front with {@link assertKnownMandates}.\n * A finding outside the classical-asymmetric scope is counted in `notInScope`;\n * an in-scope finding no selected mandate prohibits is `conformant`. Neither\n * contributes a verdict row.\n *\n * When an org `policy` is supplied (the `--policy` composition), every verdict\n * row is annotated with the org's own `policyVerdict` and an `acknowledged` flag\n * (family explicitly permitted / in-transition). Acknowledgement is purely\n * additive here — it changes no status — but {@link mandateGateFails} honours it\n * to keep the early gates from double-flagging crypto the org is knowingly,\n * traceably managing. A passed DISALLOW deadline is never acknowledgeable away.\n */\nexport function evaluateMandates(\n findings: readonly Finding[],\n mandateIdList: readonly string[],\n now: Date,\n policy?: CryptoPolicy,\n): MandateEvaluation {\n // A compliance verdict is as-of a DAY: the clauses take effect on date\n // boundaries (YYYY-MM-DD), so the exact clock time carries no compliance\n // meaning. Pin `now` to UTC midnight of its date before any arithmetic — this\n // makes the whole evaluation (statuses AND the monthsUntil / monthsUntilDisallow\n // counters) identical for any two runs on the same day, which is what keeps the\n // attested evidence hash reproducible per commit per day. Truncating changes no\n // status: every `effective` date is itself UTC-midnight, so `nowMs >= effMs` has\n // the same truth value at midnight as at any other time that day.\n const nowMs = Date.parse(`${now.toISOString().slice(0, 10)}T00:00:00.000Z`);\n const nowIso = new Date(nowMs).toISOString();\n const selected = mandateIdList.map(getMandate).filter((m): m is Mandate => Boolean(m));\n\n const rows: MandateFindingVerdict[] = [];\n const perFindingWorst: MandateStatus[] = [];\n let notInScope = 0;\n let acknowledged = 0;\n let nextDeadlineMs: number | null = null;\n\n for (const f of findings) {\n const algo = f.algorithm ?? \"unknown\";\n if (!CLASSICAL_PUBLIC_KEY.includes(algo as AlgorithmFamily)) {\n notInScope++;\n continue;\n }\n let worst: MandateStatus = \"conformant\";\n // Acknowledgement is a property of the FAMILY (fixed for this finding), so it\n // is computed once here and stamped on every row. The tally counts distinct\n // acknowledged findings (not rows), so one family under two mandates is one\n // acknowledgement, matching the per-finding status counts in `summary`.\n const family = algo as AlgorithmFamily;\n const isAcknowledged = policy ? policyAcknowledges(family, policy) : false;\n let producedRow = false;\n for (const mandate of selected) {\n // The applicable clauses for this family, earliest deadline first.\n const applicable = mandate.rules\n .filter((r) => r.prohibits.includes(algo as AlgorithmFamily))\n .sort((a, b) => a.effective.localeCompare(b.effective));\n if (applicable.length === 0) continue;\n producedRow = true;\n\n // Tier the clauses so both stay live: a passed DISALLOW clause is a\n // violation; a passed DEPRECATE clause (disallow still ahead) is the\n // deprecated warning tier; otherwise the finding is due against the next\n // upcoming clause.\n const passed = applicable.filter((r) => nowMs >= new Date(r.effective).getTime());\n const passedDisallow = passed.filter((r) => r.tier === \"disallow\");\n let status: MandateStatus;\n let governing: MandateRule;\n if (passedDisallow.length > 0) {\n status = \"violation\";\n governing = passedDisallow[0];\n } else if (passed.length > 0) {\n status = \"deprecated\";\n governing = passed[passed.length - 1];\n } else {\n status = \"due\";\n governing = applicable[0];\n }\n\n // The disallow clause (earliest, if several) anchors `leadMonths`.\n const disallowRule = applicable.find((r) => r.tier === \"disallow\") ?? null;\n const disallowMs = disallowRule ? new Date(disallowRule.effective).getTime() : null;\n\n if (status !== \"violation\") {\n for (const r of applicable) {\n const effMs = new Date(r.effective).getTime();\n if (effMs > nowMs && (nextDeadlineMs === null || effMs < nextDeadlineMs))\n nextDeadlineMs = effMs;\n }\n }\n if (STATUS_RANK[status] > STATUS_RANK[worst]) worst = status;\n rows.push({\n ruleId: f.ruleId,\n algorithm: algo,\n file: f.location.file,\n line: f.location.line,\n mandate: mandate.id,\n clause: governing.clause,\n effective: governing.effective,\n status,\n monthsUntil: monthsBetween(nowMs, new Date(governing.effective).getTime()),\n disallowEffective: disallowRule ? disallowRule.effective : null,\n monthsUntilDisallow: disallowMs !== null ? monthsBetween(nowMs, disallowMs) : null,\n citation: mandate.citation,\n policyVerdict: policy ? verdictForAlgorithm(family, policy).verdict : null,\n acknowledged: isAcknowledged,\n });\n }\n // Count the acknowledged FINDING once (it produced at least one prohibited\n // row and the org policy owns/tracks its family), not once per mandate row.\n if (producedRow && isAcknowledged) acknowledged++;\n perFindingWorst.push(worst);\n }\n\n const summary: Record<MandateStatus, number> = {\n conformant: 0,\n due: 0,\n deprecated: 0,\n violation: 0,\n };\n for (const s of perFindingWorst) summary[s]++;\n\n return {\n now: nowIso,\n mandates: selected.map((m) => m.id),\n summary,\n notInScope,\n findings: rows,\n nextDeadline:\n nextDeadlineMs !== null ? new Date(nextDeadlineMs).toISOString().slice(0, 10) : null,\n hasViolation: summary.violation > 0,\n policyName: policy?.name ?? null,\n acknowledged,\n };\n}\n\nexport interface MandateGateOptions {\n /** Fail when a DISALLOW deadline is within this many months (early enforcement). */\n leadMonths?: number;\n /** Fail on any mandate-prohibited finding regardless of the deadlines. */\n failNow?: boolean;\n}\n\n/**\n * The gate decision under the \"deadline-aware\" default: fail only once a DISALLOW\n * deadline has passed (`violation`). A passed DEPRECATE date (`deprecated`) is a\n * warning and does not fail the build. `leadMonths` fails early when a disallow\n * deadline is within the window; `failNow` fails on any prohibited finding\n * immediately.\n *\n * Policy composition: when a finding was `acknowledged` by the org policy\n * (`--policy`), it is exempt from the EARLY gates (`failNow` / `leadMonths`) —\n * the org is knowingly, traceably managing that family, so its own early\n * enforcement should not re-flag it. A passed DISALLOW deadline (`violation`)\n * still fails regardless: a dated legal disallow is not something an org can\n * self-exempt from.\n */\nexport function mandateGateFails(ev: MandateEvaluation, opts: MandateGateOptions = {}): boolean {\n if (ev.hasViolation) return true;\n // Early gates skip policy-acknowledged findings; the hard `violation` above did not.\n const gated = ev.findings.filter((v) => !v.acknowledged);\n if (opts.failNow) return gated.length > 0;\n if (opts.leadMonths !== undefined) {\n return gated.some(\n (v) => v.monthsUntilDisallow !== null && v.monthsUntilDisallow <= opts.leadMonths!,\n );\n }\n return false;\n}\n"]} |
+9
-0
@@ -10,2 +10,3 @@ /** | ||
| import type { HndlReport } from "./hndl.js"; | ||
| import type { MandateEvaluation } from "./mandates.js"; | ||
| /** Minimal SARIF 2.1.0 log shape (kept permissive on purpose). */ | ||
@@ -41,2 +42,10 @@ export interface SarifLog { | ||
| hndl?: HndlReport; | ||
| /** | ||
| * Optional compliance-mandate evaluation ({@link evaluateMandates}). When | ||
| * supplied, the JSON report carries a top-level `mandateMapping` block and the | ||
| * SARIF run carries the same under `run.properties.mandate` — the | ||
| * machine-readable half of the `--mandate` gate for CI consumption. Purely | ||
| * additive: it never changes finding identity, ordering, or exit codes. | ||
| */ | ||
| mandate?: MandateEvaluation; | ||
| } | ||
@@ -43,0 +52,0 @@ /** Serialize a scan result as SARIF 2.1.0. */ |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAA4B,QAAQ,EAAE,UAAU,EAAY,MAAM,YAAY,CAAC;AAK3F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAGhE,OAAO,KAAK,EAAmB,UAAU,EAAE,MAAM,WAAW,CAAC;AAE7D,kEAAkE;AAClE,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,OAAO,EAAE,CAAC;CACjB;AAED,qFAAqF;AACrF,MAAM,WAAW,aAAa;IAC5B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,QAAQ,EAAE,CAAC;IACrB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AA+FD,8CAA8C;AAC9C,wBAAgB,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,QAAQ,CAyI1E;AAkBD,+DAA+D;AAC/D,wBAAgB,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA6DxF;AAwCD;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,UAAU,EAClB,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,YAAY,CAAA;CAAE,GACjD,MAAM,CAyGR;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACnC,IAAI,EAAE,YAAY,GACjB,MAAM,EAAE,CAmBV;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACnC,OAAO,EAAE,gBAAgB,GACxB,MAAM,EAAE,CAsBV"} | ||
| {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAA4B,QAAQ,EAAE,UAAU,EAAY,MAAM,YAAY,CAAC;AAK3F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAGhE,OAAO,KAAK,EAAmB,UAAU,EAAE,MAAM,WAAW,CAAC;AAC7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEvD,kEAAkE;AAClE,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,OAAO,EAAE,CAAC;CACjB;AAED,qFAAqF;AACrF,MAAM,WAAW,aAAa;IAC5B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,QAAQ,EAAE,CAAC;IACrB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,iBAAiB,CAAC;CAC7B;AA+FD,8CAA8C;AAC9C,wBAAgB,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,QAAQ,CAgJ1E;AAkBD,+DAA+D;AAC/D,wBAAgB,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAkExF;AAwCD;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,UAAU,EAClB,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,YAAY,CAAA;CAAE,GACjD,MAAM,CAyGR;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACnC,IAAI,EAAE,YAAY,GACjB,MAAM,EAAE,CAmBV;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACnC,OAAO,EAAE,gBAAgB,GACxB,MAAM,EAAE,CAsBV"} |
+14
-1
@@ -198,2 +198,10 @@ import { VERSION } from "./version.js"; | ||
| : []; | ||
| // Run-level properties bag: the repo HNDL summary and/or the compliance-mandate | ||
| // evaluation, whichever were supplied. Both are additive metadata for SARIF | ||
| // consumers (our platform ingest, CI) and never affect result identity. | ||
| const runProperties = {}; | ||
| if (opts?.hndl) | ||
| runProperties.hndl = hndlSummaryBlock(opts.hndl); | ||
| if (opts?.mandate) | ||
| runProperties.mandate = opts.mandate; | ||
| return { | ||
@@ -213,3 +221,3 @@ $schema: SARIF_SCHEMA, | ||
| ...(taxonomies.length > 0 ? { taxonomies } : {}), | ||
| ...(opts?.hndl ? { properties: { hndl: hndlSummaryBlock(opts.hndl) } } : {}), | ||
| ...(Object.keys(runProperties).length > 0 ? { properties: runProperties } : {}), | ||
| results, | ||
@@ -255,2 +263,7 @@ }, | ||
| ...(hndl ? { hndl: hndlSummaryBlock(hndl) } : {}), | ||
| // Compliance-mandate evaluation (`--mandate`): the machine-readable verdicts | ||
| // + summary, so a CI job can gate/report on them without re-parsing the human | ||
| // block. Carries the org `--policy` composition (policyVerdict / acknowledged) | ||
| // when one was supplied. | ||
| ...(opts?.mandate ? { mandateMapping: opts.mandate } : {}), | ||
| findings: result.findings.map((f) => { | ||
@@ -257,0 +270,0 @@ const exposure = exposureFor(f, hndl); |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"report.js","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Reporters: turn a {@link ScanResult} into SARIF 2.1.0, a clean JSON object,\n * or a human-readable text summary. No third-party dependencies — ANSI colour\n * is emitted with raw escape codes and is off by default.\n */\nimport type { AlgorithmFamily, Finding, RuleMeta, ScanResult, Severity } from \"./types.js\";\nimport { VERSION } from \"./version.js\";\nimport { SEVERITY_ORDER, sarifLevel } from \"./severity.js\";\nimport { ANALYZABLE_LANGUAGES_LABEL } from \"./detect-utils.js\";\nimport { remediationFor, remediationForTier, remediationForProfile } from \"./remediation.js\";\nimport type { SecurityTier } from \"./remediation.js\";\nimport type { StandardsProfile } from \"./standards-profiles.js\";\nimport { fingerprintFinding } from \"./baseline.js\";\nimport { findingFingerprint } from \"./hndl.js\";\nimport type { FindingExposure, HndlReport } from \"./hndl.js\";\n\n/** Minimal SARIF 2.1.0 log shape (kept permissive on purpose). */\nexport interface SarifLog {\n $schema: string;\n version: \"2.1.0\";\n runs: unknown[];\n}\n\n/** Options shared by the structured reporters ({@link toSarif} / {@link toJson}). */\nexport interface ReportOptions {\n /**\n * Omit `location.snippet` from every finding in the output. Defaults to false\n * (snippets are included). Snippets of `sensitive` findings (e.g. PEM key\n * blocks, SSH public keys) are ALWAYS omitted regardless of this flag — the\n * snippet there IS the sensitive value.\n */\n redactSnippets?: boolean;\n /**\n * Full rule catalog to advertise in SARIF `tool.driver.rules[]`, even for\n * rules that produced no finding in this run. Pass\n * `defaultRegistry.ruleCatalog()`. When omitted, only the rules that actually\n * fired are emitted (the historical behaviour). SARIF-only; ignored by\n * {@link toJson}.\n */\n catalog?: RuleMeta[];\n /**\n * Optional HNDL exposure analysis ({@link computeHndl}). When supplied, each\n * finding gains its `exposure` fields (score, bound data asset, rationale)\n * keyed by fingerprint, and the report carries the repo-level HNDL summary.\n * Purely additive: it never changes finding identity, ordering, or exit codes.\n */\n hndl?: HndlReport;\n}\n\n/** The per-finding exposure block emitted in JSON / SARIF, or undefined. */\nfunction exposureFor(f: Finding, hndl: HndlReport | undefined): FindingExposure | undefined {\n if (!hndl) return undefined;\n return hndl.byFingerprint.get(findingFingerprint(f));\n}\n\n/** The repo HNDL summary block shared by JSON output and the SARIF run. */\nfunction hndlSummaryBlock(hndl: HndlReport): Record<string, unknown> {\n return {\n modelVersion: hndl.modelVersion,\n horizon: hndl.horizon,\n summary: hndl.summary,\n assets: hndl.assets,\n };\n}\n\nconst SARIF_SCHEMA =\n \"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json\";\n\nconst INFORMATION_URI = \"https://github.com/quantakrypto/pqc-tools\";\n\n/**\n * Resolve the snippet to emit for a finding, honouring redaction. Sensitive\n * findings (key material) never expose their snippet; otherwise the snippet is\n * dropped only when `redactSnippets` is set.\n */\nfunction emittedSnippet(f: Finding, redactSnippets: boolean): string | undefined {\n if (redactSnippets || f.sensitive) return undefined;\n return f.location.snippet;\n}\n\n/** Map our severity to a SARIF rule-level default (used in rules[].defaultConfiguration). */\nfunction sarifRank(severity: Severity): number {\n switch (severity) {\n case \"critical\":\n return 100;\n case \"high\":\n return 80;\n case \"medium\":\n return 50;\n case \"low\":\n return 20;\n default:\n return 5;\n }\n}\n\n/** Build a SARIF `rules[]` entry from a rule's severity/title/message/etc. */\nfunction sarifRule(spec: {\n id: string;\n title: string;\n message: string;\n severity: Severity;\n category: string;\n algorithm?: string;\n hndl: boolean;\n cwe?: string;\n remediation?: string;\n}): Record<string, unknown> {\n return {\n id: spec.id,\n name: spec.id,\n shortDescription: { text: spec.title },\n fullDescription: { text: spec.message },\n defaultConfiguration: { level: sarifLevel(spec.severity), rank: sarifRank(spec.severity) },\n ...(spec.remediation ? { help: { text: `Remediation: ${spec.remediation}` } } : {}),\n properties: {\n category: spec.category,\n ...(spec.algorithm ? { algorithm: spec.algorithm } : {}),\n hndl: spec.hndl,\n ...(spec.cwe ? { cwe: spec.cwe, \"security-severity\": securitySeverity(spec.severity) } : {}),\n ...(spec.cwe ? { tags: [\"security\", spec.cwe] } : {}),\n },\n ...(spec.cwe\n ? {\n relationships: [\n { target: { id: spec.cwe, toolComponent: { name: \"CWE\" } }, kinds: [\"relevant\"] },\n ],\n }\n : {}),\n };\n}\n\n/** SARIF result.properties fragment for a finding's HNDL exposure, or empty. */\nfunction exposureProperties(exposure: FindingExposure | undefined): Record<string, unknown> {\n if (!exposure) return {};\n return {\n exposureScore: exposure.exposureScore,\n dataAsset: exposure.dataAsset,\n exposureRationale: exposure.rationale,\n };\n}\n\n/** Serialize a scan result as SARIF 2.1.0. */\nexport function toSarif(result: ScanResult, opts?: ReportOptions): SarifLog {\n const redactSnippets = opts?.redactSnippets ?? false;\n // Build the rule set and collect the CWE taxa referenced by any rule. When a\n // full catalog is supplied, advertise every rule (even ones that didn't fire);\n // otherwise emit one rule per ruleId encountered (the historical behaviour).\n const ruleIndex = new Map<string, number>();\n const rules: Array<Record<string, unknown>> = [];\n const cweTaxa = new Set<string>();\n\n for (const r of opts?.catalog ?? []) {\n if (ruleIndex.has(r.id)) continue;\n if (r.cwe) cweTaxa.add(r.cwe);\n ruleIndex.set(r.id, rules.length);\n rules.push(\n sarifRule({\n id: r.id,\n title: r.title,\n message: r.message,\n severity: r.severity,\n category: r.category,\n algorithm: r.algorithm,\n hndl: r.hndl,\n cwe: r.cwe,\n remediation: r.remediation,\n }),\n );\n }\n\n for (const f of result.findings) {\n if (f.cwe) cweTaxa.add(f.cwe);\n if (ruleIndex.has(f.ruleId)) continue;\n ruleIndex.set(f.ruleId, rules.length);\n rules.push(\n sarifRule({\n id: f.ruleId,\n title: f.title,\n message: f.message,\n severity: f.severity,\n category: f.category,\n algorithm: f.algorithm,\n hndl: f.hndl,\n cwe: f.cwe,\n remediation: f.remediation,\n }),\n );\n }\n\n const results = result.findings.map((f) => {\n const region: Record<string, number> = { startLine: f.location.line };\n if (typeof f.location.column === \"number\") region.startColumn = f.location.column;\n if (typeof f.location.endLine === \"number\") region.endLine = f.location.endLine;\n const snippet = emittedSnippet(f, redactSnippets);\n\n return {\n ruleId: f.ruleId,\n ruleIndex: ruleIndex.get(f.ruleId),\n level: sarifLevel(f.severity),\n message: { text: f.message },\n // Line-INSENSITIVE fingerprint (the same one the baseline uses:\n // sha256 of ruleId|file|normalizedSnippet). GitHub code scanning keys\n // alert identity + dedup off partialFingerprints, so a finding survives\n // line shifts and reformatting instead of re-alerting as \"new\" on every\n // edit above it. `quantakrypto/v1` names our scheme.\n partialFingerprints: { \"quantakrypto/v1\": fingerprintFinding(f) },\n properties: {\n // Same stable identity mirrored into properties so non-GitHub SARIF\n // consumers (our platform ingest) can read one uniform `fingerprint`\n // field across JSON and SARIF without reaching into partialFingerprints.\n fingerprint: fingerprintFinding(f),\n category: f.category,\n severity: f.severity,\n confidence: f.confidence,\n hndl: f.hndl,\n ...(f.algorithm ? { algorithm: f.algorithm } : {}),\n ...(f.remediation ? { remediation: f.remediation } : {}),\n ...(f.cwe ? { cwe: f.cwe } : {}),\n ...exposureProperties(exposureFor(f, opts?.hndl)),\n },\n ...(f.cwe\n ? {\n taxa: [\n {\n target: { id: f.cwe, toolComponent: { name: \"CWE\" } },\n },\n ],\n }\n : {}),\n locations: [\n {\n physicalLocation: {\n artifactLocation: { uri: f.location.file },\n region: {\n ...region,\n ...(snippet ? { snippet: { text: snippet } } : {}),\n },\n },\n },\n ],\n };\n });\n\n // CWE taxonomy component (SARIF taxonomies), referenced by rules + results.\n const taxonomies =\n cweTaxa.size > 0\n ? [\n {\n name: \"CWE\",\n informationUri: \"https://cwe.mitre.org/\",\n organization: \"MITRE\",\n shortDescription: { text: \"The MITRE Common Weakness Enumeration\" },\n taxa: [...cweTaxa].sort().map((id) => ({\n id,\n helpUri: `https://cwe.mitre.org/data/definitions/${id.replace(/^CWE-/, \"\")}.html`,\n })),\n },\n ]\n : [];\n\n return {\n $schema: SARIF_SCHEMA,\n version: \"2.1.0\",\n runs: [\n {\n tool: {\n driver: {\n name: \"qScan\",\n informationUri: INFORMATION_URI,\n version: result.toolVersion || VERSION,\n rules,\n },\n },\n ...(taxonomies.length > 0 ? { taxonomies } : {}),\n ...(opts?.hndl ? { properties: { hndl: hndlSummaryBlock(opts.hndl) } } : {}),\n results,\n },\n ],\n };\n}\n\n/** GitHub-code-scanning `security-severity` (0–10) derived from our severity. */\nfunction securitySeverity(severity: Severity): string {\n switch (severity) {\n case \"critical\":\n return \"9.5\";\n case \"high\":\n return \"8.0\";\n case \"medium\":\n return \"5.0\";\n case \"low\":\n return \"3.0\";\n default:\n return \"1.0\";\n }\n}\n\n/** Serialize a scan result as a plain JSON-friendly object. */\nexport function toJson(result: ScanResult, opts?: ReportOptions): Record<string, unknown> {\n const redactSnippets = opts?.redactSnippets ?? false;\n const hndl = opts?.hndl;\n return {\n toolVersion: result.toolVersion,\n root: result.root,\n startedAt: result.startedAt,\n finishedAt: result.finishedAt,\n filesScanned: result.filesScanned,\n ...(result.analyzedFiles !== undefined ? { analyzedFiles: result.analyzedFiles } : {}),\n ...(result.diagnostics ? { diagnostics: result.diagnostics } : {}),\n inventory: {\n readinessScore: result.inventory.readinessScore,\n hndlCount: result.inventory.hndlCount,\n bySeverity: result.inventory.bySeverity,\n byCategory: result.inventory.byCategory,\n byAlgorithm: result.inventory.byAlgorithm,\n },\n ...(hndl ? { hndl: hndlSummaryBlock(hndl) } : {}),\n findings: result.findings.map((f) => {\n const exposure = exposureFor(f, hndl);\n return {\n // Stable, line-INSENSITIVE identity of the finding: sha256 of\n // ruleId | normalized-POSIX-repo-relative-path | normalized-snippet\n // (the SARIF partialFingerprints trick, line number deliberately\n // excluded). Reused verbatim from the baseline module so JSON identity,\n // SARIF partialFingerprints, and the baseline suppression set are one and\n // the same value. A line move does NOT change it; when no snippet context\n // exists it falls back to ruleId|path. This is the cross-run identity the\n // platform keys posture drift on.\n fingerprint: fingerprintFinding(f),\n ruleId: f.ruleId,\n title: f.title,\n category: f.category,\n severity: f.severity,\n confidence: f.confidence,\n algorithm: f.algorithm,\n hndl: f.hndl,\n message: f.message,\n remediation: f.remediation,\n cwe: f.cwe,\n location: {\n file: f.location.file,\n line: f.location.line,\n column: f.location.column,\n endLine: f.location.endLine,\n snippet: emittedSnippet(f, redactSnippets),\n },\n ...(exposure\n ? {\n exposure: {\n fingerprint: exposure.fingerprint,\n exposureScore: exposure.exposureScore,\n dataAsset: exposure.dataAsset,\n rationale: exposure.rationale,\n },\n }\n : {}),\n };\n }),\n };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Human-readable summary */\n/* -------------------------------------------------------------------------- */\n\n/** Raw ANSI codes (no chalk). Disabled when colour is off. */\nconst ANSI = {\n reset: \"\\x1b[0m\",\n bold: \"\\x1b[1m\",\n dim: \"\\x1b[2m\",\n red: \"\\x1b[31m\",\n green: \"\\x1b[32m\",\n yellow: \"\\x1b[33m\",\n blue: \"\\x1b[34m\",\n magenta: \"\\x1b[35m\",\n cyan: \"\\x1b[36m\",\n} as const;\n\nfunction severityColor(sev: Severity): string {\n switch (sev) {\n case \"critical\":\n return ANSI.magenta;\n case \"high\":\n return ANSI.red;\n case \"medium\":\n return ANSI.yellow;\n case \"low\":\n return ANSI.blue;\n default:\n return ANSI.dim;\n }\n}\n\nfunction scoreColor(score: number): string {\n if (score >= 80) return ANSI.green;\n if (score >= 50) return ANSI.yellow;\n return ANSI.red;\n}\n\n/**\n * Render a human-readable summary of a scan result. Colour is off by default;\n * pass `{ color: true }` to emit ANSI escape codes.\n */\nexport function formatSummary(\n result: ScanResult,\n options?: { color?: boolean; tier?: SecurityTier },\n): string {\n const color = options?.color ?? false;\n const c = (code: string, text: string): string => (color ? `${code}${text}${ANSI.reset}` : text);\n\n const lines: string[] = [];\n const inv = result.inventory;\n\n lines.push(c(ANSI.bold, \"qScan — post-quantum readiness report\"));\n lines.push(c(ANSI.dim, `tool v${result.toolVersion} · root: ${result.root}`));\n lines.push(\"\");\n\n // Readiness score banner.\n lines.push(\n `Readiness score: ${c(`${ANSI.bold}${scoreColor(inv.readinessScore)}`, `${inv.readinessScore}/100`)}`,\n );\n const analyzed =\n result.analyzedFiles !== undefined\n ? ` Analyzed (${ANALYZABLE_LANGUAGES_LABEL}): ${result.analyzedFiles}`\n : \"\";\n lines.push(\n `Files scanned: ${result.filesScanned}${analyzed} Findings: ${result.findings.length} HNDL-exposed: ${c(\n inv.hndlCount > 0 ? ANSI.red : ANSI.green,\n String(inv.hndlCount),\n )}`,\n );\n // Coverage honesty: a score over zero analyzable files is not a clean bill of health.\n if (result.analyzedFiles === 0 && result.filesScanned > 0) {\n lines.push(\n c(\n ANSI.yellow,\n `Note: 0 files were in a supported source language (${ANALYZABLE_LANGUAGES_LABEL}) — the readiness score does not reflect this codebase.`,\n ),\n );\n }\n // Coverage diagnostics: skipped files mean the finding count may be incomplete.\n const diag = result.diagnostics;\n if (diag && (diag.unreadable > 0 || diag.skippedMinified > 0)) {\n lines.push(\n c(\n ANSI.yellow,\n `Coverage: ${diag.unreadable} unreadable, ${diag.skippedMinified} skipped as minified — results may be incomplete.`,\n ),\n );\n }\n lines.push(\"\");\n\n // Severity breakdown.\n const sevParts = SEVERITY_ORDER.filter((s) => inv.bySeverity[s] > 0).map((s) =>\n c(severityColor(s), `${s}: ${inv.bySeverity[s]}`),\n );\n lines.push(`By severity: ${sevParts.length ? sevParts.join(\" \") : c(ANSI.green, \"none\")}`);\n\n // Algorithm breakdown.\n const algoParts = Object.entries(inv.byAlgorithm)\n .sort((a, b) => b[1] - a[1])\n .map(([k, v]) => `${k}: ${v}`);\n if (algoParts.length) lines.push(`By algorithm: ${algoParts.join(\" \")}`);\n lines.push(\"\");\n\n if (result.findings.length === 0) {\n lines.push(c(ANSI.green, \"No classical asymmetric cryptography detected. ✓\"));\n return lines.join(\"\\n\");\n }\n\n // Top findings, grouped by severity (most severe first), capped for readability.\n const sorted = [...result.findings].sort(\n (a, b) => SEVERITY_ORDER.indexOf(a.severity) - SEVERITY_ORDER.indexOf(b.severity),\n );\n const MAX_SHOWN = 25;\n lines.push(\n c(ANSI.bold, `Top findings (${Math.min(MAX_SHOWN, sorted.length)} of ${sorted.length}):`),\n );\n\n for (const f of sorted.slice(0, MAX_SHOWN)) {\n const loc = `${f.location.file}:${f.location.line}${\n f.location.column ? `:${f.location.column}` : \"\"\n }`;\n const tag = c(severityColor(f.severity), `[${f.severity}]`);\n const hndl = f.hndl ? c(ANSI.red, \" (HNDL)\") : \"\";\n lines.push(` ${tag} ${f.title}${hndl}`);\n lines.push(c(ANSI.dim, ` ${loc} — ${f.message}`));\n if (f.remediation) lines.push(c(ANSI.cyan, ` → ${f.remediation}`));\n }\n\n if (sorted.length > MAX_SHOWN) {\n lines.push(c(ANSI.dim, ` …and ${sorted.length - MAX_SHOWN} more.`));\n }\n\n lines.push(\"\");\n if (inv.hndlCount > 0) {\n lines.push(\n c(\n ANSI.yellow,\n `Note: ${inv.hndlCount} finding(s) are exposed to \"harvest now, decrypt later\" — ` +\n \"encrypted traffic captured today can be decrypted once a quantum computer exists. \" +\n \"Prioritise migrating key exchange / encryption to hybrid PQC (X25519MLKEM768).\",\n ),\n );\n }\n\n if (options?.tier) {\n lines.push(\"\", ...formatTierGuidance(inv.byAlgorithm, options.tier));\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Per-family migration targets for a CNSA security tier — surfaces the otherwise\n * library-only {@link remediationForTier} in human reports. Category 5 shows the\n * ML-KEM-1024 / ML-DSA-87 sets CNSA 2.0 mandates for national-security systems and\n * long-lived secrets. Returns plain (un-coloured) lines; the caller styles them.\n */\nexport function formatTierGuidance(\n byAlgorithm: Record<string, number>,\n tier: SecurityTier,\n): string[] {\n const label = tier === \"category-5\" ? \"CNSA 2.0 (Category 5)\" : \"Category 3 (commercial)\";\n const out: string[] = [`${label} migration targets:`];\n const seen = new Set<string>();\n for (const [k, n] of Object.entries(byAlgorithm)) {\n if (n <= 0) continue;\n const fam = k as AlgorithmFamily;\n if (fam === \"unknown\" || !remediationFor(fam)) continue; // skip unmapped families\n const rem = remediationForTier(fam, tier);\n if (seen.has(rem.recommendation)) continue;\n seen.add(rem.recommendation);\n out.push(` ${fam} → ${rem.recommendation}`);\n }\n if (tier === \"category-5\") {\n out.push(\n \" CNSA 2.0 mandates ML-KEM-1024 / ML-DSA-87 for national-security systems and long-lived secrets (2030/2033 milestones).\",\n );\n }\n return out;\n}\n\n/**\n * Per-family migration targets tailored to a selected {@link StandardsProfile}\n * (`--profile`). Unlike {@link formatTierGuidance} (CNSA-tier only), this surfaces the\n * regime's parameter sets AND its hybrid stance — required (ANSSI/BSI) vs recommended\n * (NIST/NCSC) vs optional (CNSA 2.0) — so guidance isn't regime-wrong. Returns plain\n * (un-coloured) lines; the caller styles them.\n */\nexport function formatProfileGuidance(\n byAlgorithm: Record<string, number>,\n profile: StandardsProfile,\n): string[] {\n const out: string[] = [`${profile.name} migration targets:`];\n const seen = new Set<string>();\n for (const [k, n] of Object.entries(byAlgorithm)) {\n if (n <= 0) continue;\n const fam = k as AlgorithmFamily;\n if (fam === \"unknown\" || !remediationFor(fam)) continue; // skip unmapped families\n const rem = remediationForProfile(fam, profile);\n if (seen.has(rem.recommendation)) continue;\n seen.add(rem.recommendation);\n out.push(` ${fam} → ${rem.recommendation}`);\n }\n const stance =\n profile.hybridStance === \"required\"\n ? \"requires classical+PQC hybridization\"\n : profile.hybridStance === \"recommended\"\n ? \"recommends hybridization\"\n : \"does not require hybridization\";\n out.push(\n ` ${profile.authority} ${stance}; classical public-key crypto disallowed after ${profile.disallowAfter} (${profile.citation}).`,\n );\n return out;\n}\n"]} | ||
| {"version":3,"file":"report.js","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Reporters: turn a {@link ScanResult} into SARIF 2.1.0, a clean JSON object,\n * or a human-readable text summary. No third-party dependencies — ANSI colour\n * is emitted with raw escape codes and is off by default.\n */\nimport type { AlgorithmFamily, Finding, RuleMeta, ScanResult, Severity } from \"./types.js\";\nimport { VERSION } from \"./version.js\";\nimport { SEVERITY_ORDER, sarifLevel } from \"./severity.js\";\nimport { ANALYZABLE_LANGUAGES_LABEL } from \"./detect-utils.js\";\nimport { remediationFor, remediationForTier, remediationForProfile } from \"./remediation.js\";\nimport type { SecurityTier } from \"./remediation.js\";\nimport type { StandardsProfile } from \"./standards-profiles.js\";\nimport { fingerprintFinding } from \"./baseline.js\";\nimport { findingFingerprint } from \"./hndl.js\";\nimport type { FindingExposure, HndlReport } from \"./hndl.js\";\nimport type { MandateEvaluation } from \"./mandates.js\";\n\n/** Minimal SARIF 2.1.0 log shape (kept permissive on purpose). */\nexport interface SarifLog {\n $schema: string;\n version: \"2.1.0\";\n runs: unknown[];\n}\n\n/** Options shared by the structured reporters ({@link toSarif} / {@link toJson}). */\nexport interface ReportOptions {\n /**\n * Omit `location.snippet` from every finding in the output. Defaults to false\n * (snippets are included). Snippets of `sensitive` findings (e.g. PEM key\n * blocks, SSH public keys) are ALWAYS omitted regardless of this flag — the\n * snippet there IS the sensitive value.\n */\n redactSnippets?: boolean;\n /**\n * Full rule catalog to advertise in SARIF `tool.driver.rules[]`, even for\n * rules that produced no finding in this run. Pass\n * `defaultRegistry.ruleCatalog()`. When omitted, only the rules that actually\n * fired are emitted (the historical behaviour). SARIF-only; ignored by\n * {@link toJson}.\n */\n catalog?: RuleMeta[];\n /**\n * Optional HNDL exposure analysis ({@link computeHndl}). When supplied, each\n * finding gains its `exposure` fields (score, bound data asset, rationale)\n * keyed by fingerprint, and the report carries the repo-level HNDL summary.\n * Purely additive: it never changes finding identity, ordering, or exit codes.\n */\n hndl?: HndlReport;\n /**\n * Optional compliance-mandate evaluation ({@link evaluateMandates}). When\n * supplied, the JSON report carries a top-level `mandateMapping` block and the\n * SARIF run carries the same under `run.properties.mandate` — the\n * machine-readable half of the `--mandate` gate for CI consumption. Purely\n * additive: it never changes finding identity, ordering, or exit codes.\n */\n mandate?: MandateEvaluation;\n}\n\n/** The per-finding exposure block emitted in JSON / SARIF, or undefined. */\nfunction exposureFor(f: Finding, hndl: HndlReport | undefined): FindingExposure | undefined {\n if (!hndl) return undefined;\n return hndl.byFingerprint.get(findingFingerprint(f));\n}\n\n/** The repo HNDL summary block shared by JSON output and the SARIF run. */\nfunction hndlSummaryBlock(hndl: HndlReport): Record<string, unknown> {\n return {\n modelVersion: hndl.modelVersion,\n horizon: hndl.horizon,\n summary: hndl.summary,\n assets: hndl.assets,\n };\n}\n\nconst SARIF_SCHEMA =\n \"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json\";\n\nconst INFORMATION_URI = \"https://github.com/quantakrypto/pqc-tools\";\n\n/**\n * Resolve the snippet to emit for a finding, honouring redaction. Sensitive\n * findings (key material) never expose their snippet; otherwise the snippet is\n * dropped only when `redactSnippets` is set.\n */\nfunction emittedSnippet(f: Finding, redactSnippets: boolean): string | undefined {\n if (redactSnippets || f.sensitive) return undefined;\n return f.location.snippet;\n}\n\n/** Map our severity to a SARIF rule-level default (used in rules[].defaultConfiguration). */\nfunction sarifRank(severity: Severity): number {\n switch (severity) {\n case \"critical\":\n return 100;\n case \"high\":\n return 80;\n case \"medium\":\n return 50;\n case \"low\":\n return 20;\n default:\n return 5;\n }\n}\n\n/** Build a SARIF `rules[]` entry from a rule's severity/title/message/etc. */\nfunction sarifRule(spec: {\n id: string;\n title: string;\n message: string;\n severity: Severity;\n category: string;\n algorithm?: string;\n hndl: boolean;\n cwe?: string;\n remediation?: string;\n}): Record<string, unknown> {\n return {\n id: spec.id,\n name: spec.id,\n shortDescription: { text: spec.title },\n fullDescription: { text: spec.message },\n defaultConfiguration: { level: sarifLevel(spec.severity), rank: sarifRank(spec.severity) },\n ...(spec.remediation ? { help: { text: `Remediation: ${spec.remediation}` } } : {}),\n properties: {\n category: spec.category,\n ...(spec.algorithm ? { algorithm: spec.algorithm } : {}),\n hndl: spec.hndl,\n ...(spec.cwe ? { cwe: spec.cwe, \"security-severity\": securitySeverity(spec.severity) } : {}),\n ...(spec.cwe ? { tags: [\"security\", spec.cwe] } : {}),\n },\n ...(spec.cwe\n ? {\n relationships: [\n { target: { id: spec.cwe, toolComponent: { name: \"CWE\" } }, kinds: [\"relevant\"] },\n ],\n }\n : {}),\n };\n}\n\n/** SARIF result.properties fragment for a finding's HNDL exposure, or empty. */\nfunction exposureProperties(exposure: FindingExposure | undefined): Record<string, unknown> {\n if (!exposure) return {};\n return {\n exposureScore: exposure.exposureScore,\n dataAsset: exposure.dataAsset,\n exposureRationale: exposure.rationale,\n };\n}\n\n/** Serialize a scan result as SARIF 2.1.0. */\nexport function toSarif(result: ScanResult, opts?: ReportOptions): SarifLog {\n const redactSnippets = opts?.redactSnippets ?? false;\n // Build the rule set and collect the CWE taxa referenced by any rule. When a\n // full catalog is supplied, advertise every rule (even ones that didn't fire);\n // otherwise emit one rule per ruleId encountered (the historical behaviour).\n const ruleIndex = new Map<string, number>();\n const rules: Array<Record<string, unknown>> = [];\n const cweTaxa = new Set<string>();\n\n for (const r of opts?.catalog ?? []) {\n if (ruleIndex.has(r.id)) continue;\n if (r.cwe) cweTaxa.add(r.cwe);\n ruleIndex.set(r.id, rules.length);\n rules.push(\n sarifRule({\n id: r.id,\n title: r.title,\n message: r.message,\n severity: r.severity,\n category: r.category,\n algorithm: r.algorithm,\n hndl: r.hndl,\n cwe: r.cwe,\n remediation: r.remediation,\n }),\n );\n }\n\n for (const f of result.findings) {\n if (f.cwe) cweTaxa.add(f.cwe);\n if (ruleIndex.has(f.ruleId)) continue;\n ruleIndex.set(f.ruleId, rules.length);\n rules.push(\n sarifRule({\n id: f.ruleId,\n title: f.title,\n message: f.message,\n severity: f.severity,\n category: f.category,\n algorithm: f.algorithm,\n hndl: f.hndl,\n cwe: f.cwe,\n remediation: f.remediation,\n }),\n );\n }\n\n const results = result.findings.map((f) => {\n const region: Record<string, number> = { startLine: f.location.line };\n if (typeof f.location.column === \"number\") region.startColumn = f.location.column;\n if (typeof f.location.endLine === \"number\") region.endLine = f.location.endLine;\n const snippet = emittedSnippet(f, redactSnippets);\n\n return {\n ruleId: f.ruleId,\n ruleIndex: ruleIndex.get(f.ruleId),\n level: sarifLevel(f.severity),\n message: { text: f.message },\n // Line-INSENSITIVE fingerprint (the same one the baseline uses:\n // sha256 of ruleId|file|normalizedSnippet). GitHub code scanning keys\n // alert identity + dedup off partialFingerprints, so a finding survives\n // line shifts and reformatting instead of re-alerting as \"new\" on every\n // edit above it. `quantakrypto/v1` names our scheme.\n partialFingerprints: { \"quantakrypto/v1\": fingerprintFinding(f) },\n properties: {\n // Same stable identity mirrored into properties so non-GitHub SARIF\n // consumers (our platform ingest) can read one uniform `fingerprint`\n // field across JSON and SARIF without reaching into partialFingerprints.\n fingerprint: fingerprintFinding(f),\n category: f.category,\n severity: f.severity,\n confidence: f.confidence,\n hndl: f.hndl,\n ...(f.algorithm ? { algorithm: f.algorithm } : {}),\n ...(f.remediation ? { remediation: f.remediation } : {}),\n ...(f.cwe ? { cwe: f.cwe } : {}),\n ...exposureProperties(exposureFor(f, opts?.hndl)),\n },\n ...(f.cwe\n ? {\n taxa: [\n {\n target: { id: f.cwe, toolComponent: { name: \"CWE\" } },\n },\n ],\n }\n : {}),\n locations: [\n {\n physicalLocation: {\n artifactLocation: { uri: f.location.file },\n region: {\n ...region,\n ...(snippet ? { snippet: { text: snippet } } : {}),\n },\n },\n },\n ],\n };\n });\n\n // CWE taxonomy component (SARIF taxonomies), referenced by rules + results.\n const taxonomies =\n cweTaxa.size > 0\n ? [\n {\n name: \"CWE\",\n informationUri: \"https://cwe.mitre.org/\",\n organization: \"MITRE\",\n shortDescription: { text: \"The MITRE Common Weakness Enumeration\" },\n taxa: [...cweTaxa].sort().map((id) => ({\n id,\n helpUri: `https://cwe.mitre.org/data/definitions/${id.replace(/^CWE-/, \"\")}.html`,\n })),\n },\n ]\n : [];\n\n // Run-level properties bag: the repo HNDL summary and/or the compliance-mandate\n // evaluation, whichever were supplied. Both are additive metadata for SARIF\n // consumers (our platform ingest, CI) and never affect result identity.\n const runProperties: Record<string, unknown> = {};\n if (opts?.hndl) runProperties.hndl = hndlSummaryBlock(opts.hndl);\n if (opts?.mandate) runProperties.mandate = opts.mandate;\n\n return {\n $schema: SARIF_SCHEMA,\n version: \"2.1.0\",\n runs: [\n {\n tool: {\n driver: {\n name: \"qScan\",\n informationUri: INFORMATION_URI,\n version: result.toolVersion || VERSION,\n rules,\n },\n },\n ...(taxonomies.length > 0 ? { taxonomies } : {}),\n ...(Object.keys(runProperties).length > 0 ? { properties: runProperties } : {}),\n results,\n },\n ],\n };\n}\n\n/** GitHub-code-scanning `security-severity` (0–10) derived from our severity. */\nfunction securitySeverity(severity: Severity): string {\n switch (severity) {\n case \"critical\":\n return \"9.5\";\n case \"high\":\n return \"8.0\";\n case \"medium\":\n return \"5.0\";\n case \"low\":\n return \"3.0\";\n default:\n return \"1.0\";\n }\n}\n\n/** Serialize a scan result as a plain JSON-friendly object. */\nexport function toJson(result: ScanResult, opts?: ReportOptions): Record<string, unknown> {\n const redactSnippets = opts?.redactSnippets ?? false;\n const hndl = opts?.hndl;\n return {\n toolVersion: result.toolVersion,\n root: result.root,\n startedAt: result.startedAt,\n finishedAt: result.finishedAt,\n filesScanned: result.filesScanned,\n ...(result.analyzedFiles !== undefined ? { analyzedFiles: result.analyzedFiles } : {}),\n ...(result.diagnostics ? { diagnostics: result.diagnostics } : {}),\n inventory: {\n readinessScore: result.inventory.readinessScore,\n hndlCount: result.inventory.hndlCount,\n bySeverity: result.inventory.bySeverity,\n byCategory: result.inventory.byCategory,\n byAlgorithm: result.inventory.byAlgorithm,\n },\n ...(hndl ? { hndl: hndlSummaryBlock(hndl) } : {}),\n // Compliance-mandate evaluation (`--mandate`): the machine-readable verdicts\n // + summary, so a CI job can gate/report on them without re-parsing the human\n // block. Carries the org `--policy` composition (policyVerdict / acknowledged)\n // when one was supplied.\n ...(opts?.mandate ? { mandateMapping: opts.mandate } : {}),\n findings: result.findings.map((f) => {\n const exposure = exposureFor(f, hndl);\n return {\n // Stable, line-INSENSITIVE identity of the finding: sha256 of\n // ruleId | normalized-POSIX-repo-relative-path | normalized-snippet\n // (the SARIF partialFingerprints trick, line number deliberately\n // excluded). Reused verbatim from the baseline module so JSON identity,\n // SARIF partialFingerprints, and the baseline suppression set are one and\n // the same value. A line move does NOT change it; when no snippet context\n // exists it falls back to ruleId|path. This is the cross-run identity the\n // platform keys posture drift on.\n fingerprint: fingerprintFinding(f),\n ruleId: f.ruleId,\n title: f.title,\n category: f.category,\n severity: f.severity,\n confidence: f.confidence,\n algorithm: f.algorithm,\n hndl: f.hndl,\n message: f.message,\n remediation: f.remediation,\n cwe: f.cwe,\n location: {\n file: f.location.file,\n line: f.location.line,\n column: f.location.column,\n endLine: f.location.endLine,\n snippet: emittedSnippet(f, redactSnippets),\n },\n ...(exposure\n ? {\n exposure: {\n fingerprint: exposure.fingerprint,\n exposureScore: exposure.exposureScore,\n dataAsset: exposure.dataAsset,\n rationale: exposure.rationale,\n },\n }\n : {}),\n };\n }),\n };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Human-readable summary */\n/* -------------------------------------------------------------------------- */\n\n/** Raw ANSI codes (no chalk). Disabled when colour is off. */\nconst ANSI = {\n reset: \"\\x1b[0m\",\n bold: \"\\x1b[1m\",\n dim: \"\\x1b[2m\",\n red: \"\\x1b[31m\",\n green: \"\\x1b[32m\",\n yellow: \"\\x1b[33m\",\n blue: \"\\x1b[34m\",\n magenta: \"\\x1b[35m\",\n cyan: \"\\x1b[36m\",\n} as const;\n\nfunction severityColor(sev: Severity): string {\n switch (sev) {\n case \"critical\":\n return ANSI.magenta;\n case \"high\":\n return ANSI.red;\n case \"medium\":\n return ANSI.yellow;\n case \"low\":\n return ANSI.blue;\n default:\n return ANSI.dim;\n }\n}\n\nfunction scoreColor(score: number): string {\n if (score >= 80) return ANSI.green;\n if (score >= 50) return ANSI.yellow;\n return ANSI.red;\n}\n\n/**\n * Render a human-readable summary of a scan result. Colour is off by default;\n * pass `{ color: true }` to emit ANSI escape codes.\n */\nexport function formatSummary(\n result: ScanResult,\n options?: { color?: boolean; tier?: SecurityTier },\n): string {\n const color = options?.color ?? false;\n const c = (code: string, text: string): string => (color ? `${code}${text}${ANSI.reset}` : text);\n\n const lines: string[] = [];\n const inv = result.inventory;\n\n lines.push(c(ANSI.bold, \"qScan — post-quantum readiness report\"));\n lines.push(c(ANSI.dim, `tool v${result.toolVersion} · root: ${result.root}`));\n lines.push(\"\");\n\n // Readiness score banner.\n lines.push(\n `Readiness score: ${c(`${ANSI.bold}${scoreColor(inv.readinessScore)}`, `${inv.readinessScore}/100`)}`,\n );\n const analyzed =\n result.analyzedFiles !== undefined\n ? ` Analyzed (${ANALYZABLE_LANGUAGES_LABEL}): ${result.analyzedFiles}`\n : \"\";\n lines.push(\n `Files scanned: ${result.filesScanned}${analyzed} Findings: ${result.findings.length} HNDL-exposed: ${c(\n inv.hndlCount > 0 ? ANSI.red : ANSI.green,\n String(inv.hndlCount),\n )}`,\n );\n // Coverage honesty: a score over zero analyzable files is not a clean bill of health.\n if (result.analyzedFiles === 0 && result.filesScanned > 0) {\n lines.push(\n c(\n ANSI.yellow,\n `Note: 0 files were in a supported source language (${ANALYZABLE_LANGUAGES_LABEL}) — the readiness score does not reflect this codebase.`,\n ),\n );\n }\n // Coverage diagnostics: skipped files mean the finding count may be incomplete.\n const diag = result.diagnostics;\n if (diag && (diag.unreadable > 0 || diag.skippedMinified > 0)) {\n lines.push(\n c(\n ANSI.yellow,\n `Coverage: ${diag.unreadable} unreadable, ${diag.skippedMinified} skipped as minified — results may be incomplete.`,\n ),\n );\n }\n lines.push(\"\");\n\n // Severity breakdown.\n const sevParts = SEVERITY_ORDER.filter((s) => inv.bySeverity[s] > 0).map((s) =>\n c(severityColor(s), `${s}: ${inv.bySeverity[s]}`),\n );\n lines.push(`By severity: ${sevParts.length ? sevParts.join(\" \") : c(ANSI.green, \"none\")}`);\n\n // Algorithm breakdown.\n const algoParts = Object.entries(inv.byAlgorithm)\n .sort((a, b) => b[1] - a[1])\n .map(([k, v]) => `${k}: ${v}`);\n if (algoParts.length) lines.push(`By algorithm: ${algoParts.join(\" \")}`);\n lines.push(\"\");\n\n if (result.findings.length === 0) {\n lines.push(c(ANSI.green, \"No classical asymmetric cryptography detected. ✓\"));\n return lines.join(\"\\n\");\n }\n\n // Top findings, grouped by severity (most severe first), capped for readability.\n const sorted = [...result.findings].sort(\n (a, b) => SEVERITY_ORDER.indexOf(a.severity) - SEVERITY_ORDER.indexOf(b.severity),\n );\n const MAX_SHOWN = 25;\n lines.push(\n c(ANSI.bold, `Top findings (${Math.min(MAX_SHOWN, sorted.length)} of ${sorted.length}):`),\n );\n\n for (const f of sorted.slice(0, MAX_SHOWN)) {\n const loc = `${f.location.file}:${f.location.line}${\n f.location.column ? `:${f.location.column}` : \"\"\n }`;\n const tag = c(severityColor(f.severity), `[${f.severity}]`);\n const hndl = f.hndl ? c(ANSI.red, \" (HNDL)\") : \"\";\n lines.push(` ${tag} ${f.title}${hndl}`);\n lines.push(c(ANSI.dim, ` ${loc} — ${f.message}`));\n if (f.remediation) lines.push(c(ANSI.cyan, ` → ${f.remediation}`));\n }\n\n if (sorted.length > MAX_SHOWN) {\n lines.push(c(ANSI.dim, ` …and ${sorted.length - MAX_SHOWN} more.`));\n }\n\n lines.push(\"\");\n if (inv.hndlCount > 0) {\n lines.push(\n c(\n ANSI.yellow,\n `Note: ${inv.hndlCount} finding(s) are exposed to \"harvest now, decrypt later\" — ` +\n \"encrypted traffic captured today can be decrypted once a quantum computer exists. \" +\n \"Prioritise migrating key exchange / encryption to hybrid PQC (X25519MLKEM768).\",\n ),\n );\n }\n\n if (options?.tier) {\n lines.push(\"\", ...formatTierGuidance(inv.byAlgorithm, options.tier));\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Per-family migration targets for a CNSA security tier — surfaces the otherwise\n * library-only {@link remediationForTier} in human reports. Category 5 shows the\n * ML-KEM-1024 / ML-DSA-87 sets CNSA 2.0 mandates for national-security systems and\n * long-lived secrets. Returns plain (un-coloured) lines; the caller styles them.\n */\nexport function formatTierGuidance(\n byAlgorithm: Record<string, number>,\n tier: SecurityTier,\n): string[] {\n const label = tier === \"category-5\" ? \"CNSA 2.0 (Category 5)\" : \"Category 3 (commercial)\";\n const out: string[] = [`${label} migration targets:`];\n const seen = new Set<string>();\n for (const [k, n] of Object.entries(byAlgorithm)) {\n if (n <= 0) continue;\n const fam = k as AlgorithmFamily;\n if (fam === \"unknown\" || !remediationFor(fam)) continue; // skip unmapped families\n const rem = remediationForTier(fam, tier);\n if (seen.has(rem.recommendation)) continue;\n seen.add(rem.recommendation);\n out.push(` ${fam} → ${rem.recommendation}`);\n }\n if (tier === \"category-5\") {\n out.push(\n \" CNSA 2.0 mandates ML-KEM-1024 / ML-DSA-87 for national-security systems and long-lived secrets (2030/2033 milestones).\",\n );\n }\n return out;\n}\n\n/**\n * Per-family migration targets tailored to a selected {@link StandardsProfile}\n * (`--profile`). Unlike {@link formatTierGuidance} (CNSA-tier only), this surfaces the\n * regime's parameter sets AND its hybrid stance — required (ANSSI/BSI) vs recommended\n * (NIST/NCSC) vs optional (CNSA 2.0) — so guidance isn't regime-wrong. Returns plain\n * (un-coloured) lines; the caller styles them.\n */\nexport function formatProfileGuidance(\n byAlgorithm: Record<string, number>,\n profile: StandardsProfile,\n): string[] {\n const out: string[] = [`${profile.name} migration targets:`];\n const seen = new Set<string>();\n for (const [k, n] of Object.entries(byAlgorithm)) {\n if (n <= 0) continue;\n const fam = k as AlgorithmFamily;\n if (fam === \"unknown\" || !remediationFor(fam)) continue; // skip unmapped families\n const rem = remediationForProfile(fam, profile);\n if (seen.has(rem.recommendation)) continue;\n seen.add(rem.recommendation);\n out.push(` ${fam} → ${rem.recommendation}`);\n }\n const stance =\n profile.hybridStance === \"required\"\n ? \"requires classical+PQC hybridization\"\n : profile.hybridStance === \"recommended\"\n ? \"recommends hybridization\"\n : \"does not require hybridization\";\n out.push(\n ` ${profile.authority} ${stance}; classical public-key crypto disallowed after ${profile.disallowAfter} (${profile.citation}).`,\n );\n return out;\n}\n"]} |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"standards-profiles.d.ts","sourceRoot":"","sources":["../src/standards-profiles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,yFAAyF;AACzF,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,aAAa,GAAG,UAAU,CAAC;AAEnE,uCAAuC;AACvC,MAAM,WAAW,gBAAgB;IAC/B,kDAAkD;IAClD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,2BAA2B;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0CAA0C;IAC1C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,gEAAgE;IAChE,QAAQ,CAAC,SAAS,EAAE;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IACzE,oFAAoF;IACpF,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,wEAAwE;IACxE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,oFAAoF;IACpF,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,yCAAyC;IACzC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,+CAA+C;IAC/C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,0EAA0E;IAC1E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,4DAA4D;AAC5D,eAAO,MAAM,kBAAkB,SAAS,CAAC;AAEzC;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAkEzE,CAAC;AAEF,mEAAmE;AACnE,wBAAgB,mBAAmB,IAAI,MAAM,EAAE,CAK9C;AAED,qEAAqE;AACrE,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAE5E;AAED,kDAAkD;AAClD,wBAAgB,uBAAuB,IAAI,gBAAgB,CAE1D"} | ||
| {"version":3,"file":"standards-profiles.d.ts","sourceRoot":"","sources":["../src/standards-profiles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,yFAAyF;AACzF,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,aAAa,GAAG,UAAU,CAAC;AAEnE,uCAAuC;AACvC,MAAM,WAAW,gBAAgB;IAC/B,kDAAkD;IAClD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,2BAA2B;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0CAA0C;IAC1C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,gEAAgE;IAChE,QAAQ,CAAC,SAAS,EAAE;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IACzE,oFAAoF;IACpF,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,wEAAwE;IACxE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,oFAAoF;IACpF,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,yCAAyC;IACzC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,+CAA+C;IAC/C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,0EAA0E;IAC1E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,4DAA4D;AAC5D,eAAO,MAAM,kBAAkB,SAAS,CAAC;AAEzC;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAuEzE,CAAC;AAEF,mEAAmE;AACnE,wBAAgB,mBAAmB,IAAI,MAAM,EAAE,CAK9C;AAED,qEAAqE;AACrE,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAE5E;AAED,kDAAkD;AAClD,wBAAgB,uBAAuB,IAAI,gBAAgB,CAE1D"} |
@@ -16,2 +16,3 @@ /** | ||
| */ | ||
| import { PQC_STANDARDS } from "./standards.js"; | ||
| /** The default profile id when `--profile` is not given. */ | ||
@@ -32,4 +33,6 @@ export const DEFAULT_PROFILE_ID = "nist"; | ||
| hybridGuidance: "Hybrid key establishment (e.g. X25519MLKEM768) is permitted and recommended during the transition; pure ML-KEM is also acceptable (SP 800-227 / IR 8547).", | ||
| deprecateAfter: 2030, | ||
| disallowAfter: 2035, | ||
| // Derived from the single source of truth so the profile can never drift from | ||
| // the `nist-ir-8547` mandate gate (the standards drift test asserts this). | ||
| deprecateAfter: PQC_STANDARDS.transitionTimeline.deprecateAfter, | ||
| disallowAfter: PQC_STANDARDS.transitionTimeline.disallowAfter, | ||
| citation: "NIST IR 8547 + FIPS 203/204/205", | ||
@@ -45,5 +48,8 @@ asOf: "2026-07", | ||
| hybridGuidance: "CNSA 2.0 targets pure PQC and does not require hybrids; if a hybrid TLS group is used, it must be SecP384r1MLKEM1024 — X25519MLKEM768's ML-KEM-768 component is sub-CNSA.", | ||
| deprecateAfter: 2030, | ||
| disallowAfter: 2035, | ||
| citation: "NSA CNSA 2.0 (2030/2033/2035 migration milestones)", | ||
| // CNSA 2.0 carries its OWN exclusive-use milestones (2030 software/firmware | ||
| // signing, 2033 general NSS) — NOT IR 8547's 2035. Derived from the single | ||
| // source so `--profile cnsa-2.0` and `--mandate cnsa-2.0` always agree. | ||
| deprecateAfter: PQC_STANDARDS.cnsaTimeline.deprecateAfter, | ||
| disallowAfter: PQC_STANDARDS.cnsaTimeline.disallowAfter, | ||
| citation: "NSA CNSA 2.0 (2030 deprecate / 2033 disallow exclusive-use milestones)", | ||
| asOf: "2026-07", | ||
@@ -50,0 +56,0 @@ }, |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"standards-profiles.js","sourceRoot":"","sources":["../src/standards-profiles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AA6BH,4DAA4D;AAC5D,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAEzC;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAA+C;IAC5E,IAAI,EAAE;QACJ,EAAE,EAAE,MAAM;QACV,IAAI,EAAE,6BAA6B;QACnC,SAAS,EAAE,MAAM;QACjB,SAAS,EAAE,EAAE,GAAG,EAAE,uBAAuB,EAAE,SAAS,EAAE,sBAAsB,EAAE;QAC9E,YAAY,EAAE,aAAa;QAC3B,cAAc,EACZ,2JAA2J;QAC7J,cAAc,EAAE,IAAI;QACpB,aAAa,EAAE,IAAI;QACnB,QAAQ,EAAE,iCAAiC;QAC3C,IAAI,EAAE,SAAS;KAChB;IACD,UAAU,EAAE;QACV,EAAE,EAAE,UAAU;QACd,IAAI,EAAE,0CAA0C;QAChD,SAAS,EAAE,KAAK;QAChB,SAAS,EAAE,EAAE,GAAG,EAAE,wBAAwB,EAAE,SAAS,EAAE,sBAAsB,EAAE;QAC/E,YAAY,EAAE,UAAU;QACxB,cAAc,EACZ,2KAA2K;QAC7K,cAAc,EAAE,IAAI;QACpB,aAAa,EAAE,IAAI;QACnB,QAAQ,EAAE,oDAAoD;QAC9D,IAAI,EAAE,SAAS;KAChB;IACD,cAAc,EAAE;QACd,EAAE,EAAE,cAAc;QAClB,IAAI,EAAE,wBAAwB;QAC9B,SAAS,EAAE,KAAK;QAChB,SAAS,EAAE,EAAE,GAAG,EAAE,uBAAuB,EAAE,SAAS,EAAE,sBAAsB,EAAE;QAC9E,YAAY,EAAE,UAAU;QACxB,cAAc,EACZ,+NAA+N;QACjO,cAAc,EAAE,IAAI;QACpB,aAAa,EAAE,IAAI;QACnB,QAAQ,EAAE,6CAA6C;QACvD,IAAI,EAAE,SAAS;KAChB;IACD,KAAK,EAAE;QACL,EAAE,EAAE,OAAO;QACX,IAAI,EAAE,gBAAgB;QACtB,SAAS,EAAE,OAAO;QAClB,SAAS,EAAE,EAAE,GAAG,EAAE,wBAAwB,EAAE,SAAS,EAAE,sBAAsB,EAAE;QAC/E,YAAY,EAAE,UAAU;QACxB,cAAc,EACZ,iLAAiL;QACnL,cAAc,EAAE,IAAI;QACpB,aAAa,EAAE,IAAI;QACnB,QAAQ,EAAE,wCAAwC;QAClD,IAAI,EAAE,SAAS;KAChB;IACD,SAAS,EAAE;QACT,EAAE,EAAE,SAAS;QACb,IAAI,EAAE,SAAS;QACf,SAAS,EAAE,MAAM;QACjB,SAAS,EAAE,EAAE,GAAG,EAAE,uBAAuB,EAAE,SAAS,EAAE,sBAAsB,EAAE;QAC9E,YAAY,EAAE,aAAa;QAC3B,cAAc,EACZ,yNAAyN;QAC3N,cAAc,EAAE,IAAI;QACpB,aAAa,EAAE,IAAI;QACnB,QAAQ,EAAE,yEAAyE;QACnF,IAAI,EAAE,SAAS;KAChB;CACF,CAAC;AAEF,mEAAmE;AACnE,MAAM,UAAU,mBAAmB;IACjC,OAAO;QACL,kBAAkB;QAClB,GAAG,MAAM,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,KAAK,kBAAkB,CAAC;KAC7E,CAAC;AACJ,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,mBAAmB,CAAC,EAAU;IAC5C,OAAO,kBAAkB,CAAC,EAAE,CAAC,CAAC;AAChC,CAAC;AAED,kDAAkD;AAClD,MAAM,UAAU,uBAAuB;IACrC,OAAO,kBAAkB,CAAC,kBAAkB,CAAC,CAAC;AAChD,CAAC","sourcesContent":["/**\n * Selectable STANDARDS PROFILES — the regime a scan's remediation, deadlines, and\n * hybrid guidance are tailored to. Different national/regional authorities agree on\n * the PQC primitives (ML-KEM / ML-DSA) but diverge on two things this tool must not\n * hardcode: (a) the required PARAMETER SETS (a commercial ML-KEM-768 vs a\n * national-security ML-KEM-1024), and (b) the HYBRID STANCE — whether classical+PQC\n * hybridization is required, recommended, or optional during the transition. Baking\n * in NIST/CNSA's \"hybrids optional\" is wrong for an ANSSI or BSI audience, where\n * hybrid is required. `--profile <id>` selects the regime; `--policy` composes an\n * org's own exceptions on top.\n *\n * Like {@link PqcStandards}, every profile carries a citation + `asOf` date and is\n * re-verified on the quarterly standards cadence. These are guidance summaries, not\n * legal advice — consult the cited source of record.\n */\n\n/** Whether classical+PQC hybridization is required during the transition, per regime. */\nexport type HybridStance = \"required\" | \"recommended\" | \"optional\";\n\n/** A regime's PQC guidance profile. */\nexport interface StandardsProfile {\n /** Stable id used by `--profile` (kebab-case). */\n readonly id: string;\n /** Human-readable name. */\n readonly name: string;\n /** The authority / document of record. */\n readonly authority: string;\n /** The KEM / signature parameter sets this regime calls for. */\n readonly paramSets: { readonly kem: string; readonly signature: string };\n /** Whether hybridization is required / recommended / optional under this regime. */\n readonly hybridStance: HybridStance;\n /** One-line regime-specific hybrid guidance surfaced in remediation. */\n readonly hybridGuidance: string;\n /** Year after which classical public-key crypto is deprecated under this regime. */\n readonly deprecateAfter: number;\n /** Year after which it is disallowed. */\n readonly disallowAfter: number;\n /** Spec identifier / publication of record. */\n readonly citation: string;\n /** `YYYY-MM` — when this profile was last verified against its source. */\n readonly asOf: string;\n}\n\n/** The default profile id when `--profile` is not given. */\nexport const DEFAULT_PROFILE_ID = \"nist\";\n\n/**\n * Built-in regime profiles. Facts reflect each authority's published PQC-transition\n * position as of the `asOf` date; verify against the cited source before relying on a\n * deadline or a hybrid mandate for a compliance decision.\n */\nexport const STANDARDS_PROFILES: Readonly<Record<string, StandardsProfile>> = {\n nist: {\n id: \"nist\",\n name: \"NIST (general / commercial)\",\n authority: \"NIST\",\n paramSets: { kem: \"ML-KEM-768 (FIPS 203)\", signature: \"ML-DSA-65 (FIPS 204)\" },\n hybridStance: \"recommended\",\n hybridGuidance:\n \"Hybrid key establishment (e.g. X25519MLKEM768) is permitted and recommended during the transition; pure ML-KEM is also acceptable (SP 800-227 / IR 8547).\",\n deprecateAfter: 2030,\n disallowAfter: 2035,\n citation: \"NIST IR 8547 + FIPS 203/204/205\",\n asOf: \"2026-07\",\n },\n \"cnsa-2.0\": {\n id: \"cnsa-2.0\",\n name: \"NSA CNSA 2.0 (national-security systems)\",\n authority: \"NSA\",\n paramSets: { kem: \"ML-KEM-1024 (FIPS 203)\", signature: \"ML-DSA-87 (FIPS 204)\" },\n hybridStance: \"optional\",\n hybridGuidance:\n \"CNSA 2.0 targets pure PQC and does not require hybrids; if a hybrid TLS group is used, it must be SecP384r1MLKEM1024 — X25519MLKEM768's ML-KEM-768 component is sub-CNSA.\",\n deprecateAfter: 2030,\n disallowAfter: 2035,\n citation: \"NSA CNSA 2.0 (2030/2033/2035 migration milestones)\",\n asOf: \"2026-07\",\n },\n \"bsi-tr-02102\": {\n id: \"bsi-tr-02102\",\n name: \"BSI TR-02102 (Germany)\",\n authority: \"BSI\",\n paramSets: { kem: \"ML-KEM-768 (FIPS 203)\", signature: \"ML-DSA-65 (FIPS 204)\" },\n hybridStance: \"required\",\n hybridGuidance:\n \"BSI requires PQC be deployed in HYBRID with an established classical scheme during the transition (defense-in-depth); FrodoKEM is the conservative KEM alternative to ML-KEM, and XMSS/LMS are approved for firmware signing.\",\n deprecateAfter: 2030,\n disallowAfter: 2035,\n citation: \"BSI TR-02102-1 (Kryptographische Verfahren)\",\n asOf: \"2026-07\",\n },\n anssi: {\n id: \"anssi\",\n name: \"ANSSI (France)\",\n authority: \"ANSSI\",\n paramSets: { kem: \"ML-KEM-1024 (FIPS 203)\", signature: \"ML-DSA-87 (FIPS 204)\" },\n hybridStance: \"required\",\n hybridGuidance:\n \"ANSSI requires HYBRIDIZATION (classical + PQC) throughout the transition phase and does not endorse pure PQC alone yet; use the highest parameter set for long-lived assurance.\",\n deprecateAfter: 2030,\n disallowAfter: 2035,\n citation: \"ANSSI — PQC transition position papers\",\n asOf: \"2026-07\",\n },\n \"uk-ncsc\": {\n id: \"uk-ncsc\",\n name: \"UK NCSC\",\n authority: \"NCSC\",\n paramSets: { kem: \"ML-KEM-768 (FIPS 203)\", signature: \"ML-DSA-65 (FIPS 204)\" },\n hybridStance: \"recommended\",\n hybridGuidance:\n \"NCSC recommends ML-KEM / ML-DSA and is broadly agnostic on hybridization (recommended, not mandated); its migration milestones are earlier — discovery/plan by 2028, high-priority migration by 2031, complete by 2035.\",\n deprecateAfter: 2031,\n disallowAfter: 2035,\n citation: \"NCSC — Preparing for quantum-safe cryptography / PQC migration timeline\",\n asOf: \"2026-07\",\n },\n};\n\n/** All built-in profile ids, in a stable order (default first). */\nexport function standardsProfileIds(): string[] {\n return [\n DEFAULT_PROFILE_ID,\n ...Object.keys(STANDARDS_PROFILES).filter((id) => id !== DEFAULT_PROFILE_ID),\n ];\n}\n\n/** Look up a built-in profile by id, or `undefined` when unknown. */\nexport function getStandardsProfile(id: string): StandardsProfile | undefined {\n return STANDARDS_PROFILES[id];\n}\n\n/** The default profile (NIST). Always defined. */\nexport function defaultStandardsProfile(): StandardsProfile {\n return STANDARDS_PROFILES[DEFAULT_PROFILE_ID];\n}\n"]} | ||
| {"version":3,"file":"standards-profiles.js","sourceRoot":"","sources":["../src/standards-profiles.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Selectable STANDARDS PROFILES — the regime a scan's remediation, deadlines, and\n * hybrid guidance are tailored to. Different national/regional authorities agree on\n * the PQC primitives (ML-KEM / ML-DSA) but diverge on two things this tool must not\n * hardcode: (a) the required PARAMETER SETS (a commercial ML-KEM-768 vs a\n * national-security ML-KEM-1024), and (b) the HYBRID STANCE — whether classical+PQC\n * hybridization is required, recommended, or optional during the transition. Baking\n * in NIST/CNSA's \"hybrids optional\" is wrong for an ANSSI or BSI audience, where\n * hybrid is required. `--profile <id>` selects the regime; `--policy` composes an\n * org's own exceptions on top.\n *\n * Like {@link PqcStandards}, every profile carries a citation + `asOf` date and is\n * re-verified on the quarterly standards cadence. These are guidance summaries, not\n * legal advice — consult the cited source of record.\n */\n\nimport { PQC_STANDARDS } from \"./standards.js\";\n\n/** Whether classical+PQC hybridization is required during the transition, per regime. */\nexport type HybridStance = \"required\" | \"recommended\" | \"optional\";\n\n/** A regime's PQC guidance profile. */\nexport interface StandardsProfile {\n /** Stable id used by `--profile` (kebab-case). */\n readonly id: string;\n /** Human-readable name. */\n readonly name: string;\n /** The authority / document of record. */\n readonly authority: string;\n /** The KEM / signature parameter sets this regime calls for. */\n readonly paramSets: { readonly kem: string; readonly signature: string };\n /** Whether hybridization is required / recommended / optional under this regime. */\n readonly hybridStance: HybridStance;\n /** One-line regime-specific hybrid guidance surfaced in remediation. */\n readonly hybridGuidance: string;\n /** Year after which classical public-key crypto is deprecated under this regime. */\n readonly deprecateAfter: number;\n /** Year after which it is disallowed. */\n readonly disallowAfter: number;\n /** Spec identifier / publication of record. */\n readonly citation: string;\n /** `YYYY-MM` — when this profile was last verified against its source. */\n readonly asOf: string;\n}\n\n/** The default profile id when `--profile` is not given. */\nexport const DEFAULT_PROFILE_ID = \"nist\";\n\n/**\n * Built-in regime profiles. Facts reflect each authority's published PQC-transition\n * position as of the `asOf` date; verify against the cited source before relying on a\n * deadline or a hybrid mandate for a compliance decision.\n */\nexport const STANDARDS_PROFILES: Readonly<Record<string, StandardsProfile>> = {\n nist: {\n id: \"nist\",\n name: \"NIST (general / commercial)\",\n authority: \"NIST\",\n paramSets: { kem: \"ML-KEM-768 (FIPS 203)\", signature: \"ML-DSA-65 (FIPS 204)\" },\n hybridStance: \"recommended\",\n hybridGuidance:\n \"Hybrid key establishment (e.g. X25519MLKEM768) is permitted and recommended during the transition; pure ML-KEM is also acceptable (SP 800-227 / IR 8547).\",\n // Derived from the single source of truth so the profile can never drift from\n // the `nist-ir-8547` mandate gate (the standards drift test asserts this).\n deprecateAfter: PQC_STANDARDS.transitionTimeline.deprecateAfter,\n disallowAfter: PQC_STANDARDS.transitionTimeline.disallowAfter,\n citation: \"NIST IR 8547 + FIPS 203/204/205\",\n asOf: \"2026-07\",\n },\n \"cnsa-2.0\": {\n id: \"cnsa-2.0\",\n name: \"NSA CNSA 2.0 (national-security systems)\",\n authority: \"NSA\",\n paramSets: { kem: \"ML-KEM-1024 (FIPS 203)\", signature: \"ML-DSA-87 (FIPS 204)\" },\n hybridStance: \"optional\",\n hybridGuidance:\n \"CNSA 2.0 targets pure PQC and does not require hybrids; if a hybrid TLS group is used, it must be SecP384r1MLKEM1024 — X25519MLKEM768's ML-KEM-768 component is sub-CNSA.\",\n // CNSA 2.0 carries its OWN exclusive-use milestones (2030 software/firmware\n // signing, 2033 general NSS) — NOT IR 8547's 2035. Derived from the single\n // source so `--profile cnsa-2.0` and `--mandate cnsa-2.0` always agree.\n deprecateAfter: PQC_STANDARDS.cnsaTimeline.deprecateAfter,\n disallowAfter: PQC_STANDARDS.cnsaTimeline.disallowAfter,\n citation: \"NSA CNSA 2.0 (2030 deprecate / 2033 disallow exclusive-use milestones)\",\n asOf: \"2026-07\",\n },\n \"bsi-tr-02102\": {\n id: \"bsi-tr-02102\",\n name: \"BSI TR-02102 (Germany)\",\n authority: \"BSI\",\n paramSets: { kem: \"ML-KEM-768 (FIPS 203)\", signature: \"ML-DSA-65 (FIPS 204)\" },\n hybridStance: \"required\",\n hybridGuidance:\n \"BSI requires PQC be deployed in HYBRID with an established classical scheme during the transition (defense-in-depth); FrodoKEM is the conservative KEM alternative to ML-KEM, and XMSS/LMS are approved for firmware signing.\",\n deprecateAfter: 2030,\n disallowAfter: 2035,\n citation: \"BSI TR-02102-1 (Kryptographische Verfahren)\",\n asOf: \"2026-07\",\n },\n anssi: {\n id: \"anssi\",\n name: \"ANSSI (France)\",\n authority: \"ANSSI\",\n paramSets: { kem: \"ML-KEM-1024 (FIPS 203)\", signature: \"ML-DSA-87 (FIPS 204)\" },\n hybridStance: \"required\",\n hybridGuidance:\n \"ANSSI requires HYBRIDIZATION (classical + PQC) throughout the transition phase and does not endorse pure PQC alone yet; use the highest parameter set for long-lived assurance.\",\n deprecateAfter: 2030,\n disallowAfter: 2035,\n citation: \"ANSSI — PQC transition position papers\",\n asOf: \"2026-07\",\n },\n \"uk-ncsc\": {\n id: \"uk-ncsc\",\n name: \"UK NCSC\",\n authority: \"NCSC\",\n paramSets: { kem: \"ML-KEM-768 (FIPS 203)\", signature: \"ML-DSA-65 (FIPS 204)\" },\n hybridStance: \"recommended\",\n hybridGuidance:\n \"NCSC recommends ML-KEM / ML-DSA and is broadly agnostic on hybridization (recommended, not mandated); its migration milestones are earlier — discovery/plan by 2028, high-priority migration by 2031, complete by 2035.\",\n deprecateAfter: 2031,\n disallowAfter: 2035,\n citation: \"NCSC — Preparing for quantum-safe cryptography / PQC migration timeline\",\n asOf: \"2026-07\",\n },\n};\n\n/** All built-in profile ids, in a stable order (default first). */\nexport function standardsProfileIds(): string[] {\n return [\n DEFAULT_PROFILE_ID,\n ...Object.keys(STANDARDS_PROFILES).filter((id) => id !== DEFAULT_PROFILE_ID),\n ];\n}\n\n/** Look up a built-in profile by id, or `undefined` when unknown. */\nexport function getStandardsProfile(id: string): StandardsProfile | undefined {\n return STANDARDS_PROFILES[id];\n}\n\n/** The default profile (NIST). Always defined. */\nexport function defaultStandardsProfile(): StandardsProfile {\n return STANDARDS_PROFILES[DEFAULT_PROFILE_ID];\n}\n"]} |
+16
-1
@@ -62,3 +62,3 @@ /** | ||
| readonly statefulHbs: StandardsCitation; | ||
| /** The migration deadline the transition note surfaces. */ | ||
| /** The NIST IR 8547 migration deadline the transition note surfaces. */ | ||
| readonly transitionTimeline: { | ||
@@ -72,2 +72,17 @@ /** Year after which classical public-key crypto is deprecated. */ | ||
| }; | ||
| /** | ||
| * CNSA 2.0's OWN migration milestones — distinct from the IR 8547 timeline | ||
| * above. CNSA 2.0 sets exclusive-use dates per system class; the `cnsa-2.0` | ||
| * mandate encodes the earliest hard milestone as `deprecate` and the general | ||
| * exclusive-use milestone as `disallow`. Kept separate so the two mandates | ||
| * ({@link MANDATES}) each derive from their own dated source. | ||
| */ | ||
| readonly cnsaTimeline: { | ||
| /** Year after which classical PKC is deprecated (2030: software/firmware signing exclusive). */ | ||
| readonly deprecateAfter: number; | ||
| /** Year after which it is disallowed (2033: general NSS exclusive use). */ | ||
| readonly disallowAfter: number; | ||
| readonly source: string; | ||
| readonly asOf: string; | ||
| }; | ||
| /** Emerging / backup standards worth tracking beyond the current FIPS. */ | ||
@@ -74,0 +89,0 @@ readonly emerging: readonly StandardsCitation[]; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"standards.d.ts","sourceRoot":"","sources":["../src/standards.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,+EAA+E;AAC/E,MAAM,WAAW,iBAAiB;IAChC,sCAAsC;IACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,4DAA4D;IAC5D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,gEAAgE;AAChE,MAAM,WAAW,YAAY;IAC3B,gEAAgE;IAChE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,kDAAkD;IAClD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,2CAA2C;IAC3C,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IAEtC,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,EAAE;QACb,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC;QAClC,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC;QAClC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;KACpC,CAAC;IAEF;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE;QACb,QAAQ,CAAC,SAAS,EAAE;YAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;SAAE,CAAC;QACzE,QAAQ,CAAC,SAAS,EAAE;YAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;SAAE,CAAC;QACzE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;KACvB,CAAC;IAEF,gEAAgE;IAChE,QAAQ,CAAC,WAAW,EAAE,iBAAiB,CAAC;IAExC,2DAA2D;IAC3D,QAAQ,CAAC,kBAAkB,EAAE;QAC3B,kEAAkE;QAClE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;QAChC,yCAAyC;QACzC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;QAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;KACvB,CAAC;IAEF,0EAA0E;IAC1E,QAAQ,CAAC,QAAQ,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAEhD,8CAA8C;IAC9C,QAAQ,CAAC,OAAO,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAChD;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,YA6E3B,CAAC;AAEF,+CAA+C;AAC/C,MAAM,WAAW,qBAAqB;IACpC,qEAAqE;IACrE,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,mEAAmE;IACnE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,wEAAwE;IACxE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,IAAI,EACT,SAAS,GAAE,YAA4B,GACtC,qBAAqB,CAMvB"} | ||
| {"version":3,"file":"standards.d.ts","sourceRoot":"","sources":["../src/standards.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,+EAA+E;AAC/E,MAAM,WAAW,iBAAiB;IAChC,sCAAsC;IACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,4DAA4D;IAC5D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,gEAAgE;AAChE,MAAM,WAAW,YAAY;IAC3B,gEAAgE;IAChE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,kDAAkD;IAClD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,2CAA2C;IAC3C,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IAEtC,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,EAAE;QACb,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC;QAClC,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC;QAClC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;KACpC,CAAC;IAEF;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE;QACb,QAAQ,CAAC,SAAS,EAAE;YAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;SAAE,CAAC;QACzE,QAAQ,CAAC,SAAS,EAAE;YAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;SAAE,CAAC;QACzE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;KACvB,CAAC;IAEF,gEAAgE;IAChE,QAAQ,CAAC,WAAW,EAAE,iBAAiB,CAAC;IAExC,wEAAwE;IACxE,QAAQ,CAAC,kBAAkB,EAAE;QAC3B,kEAAkE;QAClE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;QAChC,yCAAyC;QACzC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;QAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;KACvB,CAAC;IAEF;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,EAAE;QACrB,gGAAgG;QAChG,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;QAChC,2EAA2E;QAC3E,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;QAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;KACvB,CAAC;IAEF,0EAA0E;IAC1E,QAAQ,CAAC,QAAQ,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAEhD,8CAA8C;IAC9C,QAAQ,CAAC,OAAO,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAChD;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,YAqF3B,CAAC;AAEF,+CAA+C;AAC/C,MAAM,WAAW,qBAAqB;IACpC,qEAAqE;IACrE,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,mEAAmE;IACnE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,wEAAwE;IACxE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,IAAI,EACT,SAAS,GAAE,YAA4B,GACtC,qBAAqB,CAMvB"} |
@@ -64,2 +64,8 @@ /** | ||
| }, | ||
| cnsaTimeline: { | ||
| deprecateAfter: 2030, | ||
| disallowAfter: 2033, | ||
| source: "NSA CNSA 2.0 (exclusive-use milestones: 2030 software/firmware signing, 2033 general NSS)", | ||
| asOf: "2026-07", | ||
| }, | ||
| emerging: [ | ||
@@ -66,0 +72,0 @@ { |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"standards.js","sourceRoot":"","sources":["../src/standards.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AA2DH;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAiB;IACzC,YAAY,EAAE,YAAY;IAC1B,UAAU,EAAE,YAAY;IACxB,oBAAoB,EAAE,CAAC;IAEvB,IAAI,EAAE;QACJ,KAAK,EAAE;YACL,OAAO,EAAE,2DAA2D;YACpE,MAAM,EAAE,eAAe;YACvB,IAAI,EAAE,SAAS;SAChB;QACD,KAAK,EAAE;YACL,OAAO,EAAE,gEAAgE;YACzE,MAAM,EAAE,eAAe;YACvB,IAAI,EAAE,SAAS;SAChB;QACD,MAAM,EAAE;YACN,OAAO,EAAE,6EAA6E;YACtF,MAAM,EAAE,eAAe;YACvB,IAAI,EAAE,SAAS;SAChB;KACF;IAED,IAAI,EAAE;QACJ,SAAS,EAAE,EAAE,GAAG,EAAE,uBAAuB,EAAE,SAAS,EAAE,sBAAsB,EAAE;QAC9E,SAAS,EAAE,EAAE,GAAG,EAAE,wBAAwB,EAAE,SAAS,EAAE,sBAAsB,EAAE;QAC/E,MAAM,EAAE,0EAA0E;QAClF,IAAI,EAAE,SAAS;KAChB;IAED,WAAW,EAAE;QACX,OAAO,EACL,iFAAiF;YACjF,mFAAmF;QACrF,MAAM,EAAE,iBAAiB;QACzB,IAAI,EAAE,SAAS;KAChB;IAED,kBAAkB,EAAE;QAClB,cAAc,EAAE,IAAI;QACpB,aAAa,EAAE,IAAI;QACnB,MAAM,EAAE,kEAAkE;QAC1E,IAAI,EAAE,SAAS;KAChB;IAED,QAAQ,EAAE;QACR;YACE,OAAO,EACL,yFAAyF;gBACzF,sDAAsD;YACxD,MAAM,EAAE,0BAA0B;YAClC,IAAI,EAAE,SAAS;SAChB;QACD;YACE,OAAO,EAAE,+CAA+C;YACxD,MAAM,EAAE,qBAAqB;YAC7B,IAAI,EAAE,SAAS;SAChB;QACD;YACE,OAAO,EAAE,oEAAoE;YAC7E,MAAM,EAAE,oCAAoC;YAC5C,IAAI,EAAE,SAAS;SAChB;KACF;IAED,OAAO,EAAE;QACP;YACE,OAAO,EAAE,iEAAiE;YAC1E,MAAM,EAAE,iCAAiC;YACzC,IAAI,EAAE,SAAS;SAChB;QACD;YACE,OAAO,EAAE,uEAAuE;YAChF,MAAM,EAAE,iCAAiC;YACzC,IAAI,EAAE,SAAS;SAChB;KACF;CACF,CAAC;AAYF;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CACnC,GAAS,EACT,YAA0B,aAAa;IAEvC,MAAM,UAAU,GAAG,UAAU,CAAC;IAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,SAAS,CAAC,UAAU,YAAY,CAAC,CAAC;IAC7D,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,EAAE,GAAG,CAAC,WAAW,EAAE,EAAE,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC;IAClF,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,GAAG,UAAU,CAAC,CAAC;IAC1D,OAAO,EAAE,GAAG,EAAE,SAAS,IAAI,CAAC,EAAE,UAAU,EAAE,SAAS,CAAC,UAAU,EAAE,SAAS,EAAE,CAAC;AAC9E,CAAC","sourcesContent":["/**\n * Single source of truth for the post-quantum standards this tool depends on.\n *\n * The scanner's credibility rests on its recommendations tracking the current\n * NIST / CNSA / IETF state. That tracking used to be ad-hoc — facts were spread\n * across `remediation.ts` with no dates, no citations, and nothing to catch code\n * drifting from the published standards. This module makes the standards facts\n * explicit, dated, and cited, and the companion drift test\n * (`test/standards.test.ts`) fails the build if the runtime constants\n * (`TIER_PARAMS`, `PQC_TRANSITION_NOTE`, `STATEFUL_HBS_NOTE`) fall out of sync\n * with what is recorded here.\n *\n * ## Cadence\n *\n * Re-verify every quarter (see `docs/standards/pqc-standards.md` for the runbook).\n * On each review: check the sources below for changes, update the facts + their\n * `asOf`, and roll `lastReviewed` / `nextReview` forward. `scripts/standards-check.mjs`\n * (advisory, runs in CI) flags when `nextReview` has passed so a review can't be\n * silently skipped. `standardsReviewStatus(now)` is the pure predicate behind it.\n */\n\n/** A single standards fact with its citation and when it was last verified. */\nexport interface StandardsCitation {\n /** One-line statement of the fact. */\n readonly summary: string;\n /** Spec identifier / publication (and URL where stable). */\n readonly source: string;\n /** `YYYY-MM` — when this fact was last verified against its source. */\n readonly asOf: string;\n}\n\n/** The full post-quantum standards snapshot the tool tracks. */\nexport interface PqcStandards {\n /** `YYYY-MM-DD` — when the whole snapshot was last reviewed. */\n readonly lastReviewed: string;\n /** `YYYY-MM-DD` — when the next review is due. */\n readonly nextReview: string;\n /** Cadence, in months, between reviews. */\n readonly reviewIntervalMonths: number;\n\n /** NIST's finalized PQC FIPS (the recommendation targets). */\n readonly fips: {\n readonly mlKem: StandardsCitation; // FIPS 203\n readonly mlDsa: StandardsCitation; // FIPS 204\n readonly slhDsa: StandardsCitation; // FIPS 205\n };\n\n /**\n * CNSA 2.0 security tiers → the KEM / signature parameter sets. These MUST\n * mirror `remediation.TIER_PARAMS`; the drift test asserts they stay identical.\n */\n readonly cnsa: {\n readonly category3: { readonly kem: string; readonly signature: string };\n readonly category5: { readonly kem: string; readonly signature: string };\n readonly source: string;\n readonly asOf: string;\n };\n\n /** Stateful hash-based signatures (firmware / boot signing). */\n readonly statefulHbs: StandardsCitation; // SP 800-208\n\n /** The migration deadline the transition note surfaces. */\n readonly transitionTimeline: {\n /** Year after which classical public-key crypto is deprecated. */\n readonly deprecateAfter: number;\n /** Year after which it is disallowed. */\n readonly disallowAfter: number;\n readonly source: string;\n readonly asOf: string;\n };\n\n /** Emerging / backup standards worth tracking beyond the current FIPS. */\n readonly emerging: readonly StandardsCitation[];\n\n /** Recommended hybrid key-exchange groups. */\n readonly hybrids: readonly StandardsCitation[];\n}\n\n/**\n * The current snapshot. Update on each quarterly review; the drift test keeps the\n * runtime remediation constants aligned with it.\n */\nexport const PQC_STANDARDS: PqcStandards = {\n lastReviewed: \"2026-07-19\",\n nextReview: \"2026-10-19\",\n reviewIntervalMonths: 3,\n\n fips: {\n mlKem: {\n summary: \"ML-KEM (Kyber) key encapsulation — finalized August 2024.\",\n source: \"NIST FIPS 203\",\n asOf: \"2026-07\",\n },\n mlDsa: {\n summary: \"ML-DSA (Dilithium) lattice signatures — finalized August 2024.\",\n source: \"NIST FIPS 204\",\n asOf: \"2026-07\",\n },\n slhDsa: {\n summary: \"SLH-DSA (SPHINCS+) stateless hash-based signatures — finalized August 2024.\",\n source: \"NIST FIPS 205\",\n asOf: \"2026-07\",\n },\n },\n\n cnsa: {\n category3: { kem: \"ML-KEM-768 (FIPS 203)\", signature: \"ML-DSA-65 (FIPS 204)\" },\n category5: { kem: \"ML-KEM-1024 (FIPS 203)\", signature: \"ML-DSA-87 (FIPS 204)\" },\n source: \"NSA CNSA 2.0 (national-security systems; 2030/2033 migration milestones)\",\n asOf: \"2026-07\",\n },\n\n statefulHbs: {\n summary:\n \"LMS/HSS and XMSS/XMSSMT stateful hash-based signatures (incl. the SHAKE256 and \" +\n \"192-bit parameter sets) are approved for firmware/boot signing, but are STATEFUL.\",\n source: \"NIST SP 800-208\",\n asOf: \"2026-07\",\n },\n\n transitionTimeline: {\n deprecateAfter: 2030,\n disallowAfter: 2035,\n source: \"NIST IR 8547 (transition to post-quantum cryptography standards)\",\n asOf: \"2026-07\",\n },\n\n emerging: [\n {\n summary:\n \"HQC — NIST's code-based backup KEM (selected March 2025; draft FIPS expected ~2026), a \" +\n \"diversity hedge against ML-KEM's lattice assumption.\",\n source: \"NIST PQC (HQC selection)\",\n asOf: \"2026-07\",\n },\n {\n summary: \"FN-DSA / Falcon — compact lattice signatures.\",\n source: \"NIST draft FIPS 206\",\n asOf: \"2026-07\",\n },\n {\n summary: \"X-Wing — X25519 + ML-KEM-768 hybrid KEM for HPKE-style encryption.\",\n source: \"IETF draft-connolly-cfrg-xwing-kem\",\n asOf: \"2026-07\",\n },\n ],\n\n hybrids: [\n {\n summary: \"X25519MLKEM768 — the default TLS 1.3 hybrid key-exchange group.\",\n source: \"IETF draft-ietf-tls-ecdhe-mlkem\",\n asOf: \"2026-07\",\n },\n {\n summary: \"SecP384r1MLKEM1024 — the Category-5 / CNSA hybrid key-exchange group.\",\n source: \"IETF draft-ietf-tls-ecdhe-mlkem\",\n asOf: \"2026-07\",\n },\n ],\n};\n\n/** Result of {@link standardsReviewStatus}. */\nexport interface StandardsReviewStatus {\n /** True when `now` is on or after `nextReview` — a review is due. */\n readonly due: boolean;\n /** The `nextReview` date being compared against (`YYYY-MM-DD`). */\n readonly nextReview: string;\n /** Whole days from `now` until `nextReview` (negative when overdue). */\n readonly daysUntil: number;\n}\n\n/**\n * Whether a standards review is due as of `now`. Pure (takes `now` explicitly) so\n * it is deterministic in tests; the CI script passes the real clock. Compares on\n * whole UTC days so a same-day run is not spuriously \"overdue\".\n */\nexport function standardsReviewStatus(\n now: Date,\n standards: PqcStandards = PQC_STANDARDS,\n): StandardsReviewStatus {\n const MS_PER_DAY = 86_400_000;\n const next = Date.parse(`${standards.nextReview}T00:00:00Z`);\n const today = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());\n const daysUntil = Math.round((next - today) / MS_PER_DAY);\n return { due: daysUntil <= 0, nextReview: standards.nextReview, daysUntil };\n}\n"]} | ||
| {"version":3,"file":"standards.js","sourceRoot":"","sources":["../src/standards.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AA2EH;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAiB;IACzC,YAAY,EAAE,YAAY;IAC1B,UAAU,EAAE,YAAY;IACxB,oBAAoB,EAAE,CAAC;IAEvB,IAAI,EAAE;QACJ,KAAK,EAAE;YACL,OAAO,EAAE,2DAA2D;YACpE,MAAM,EAAE,eAAe;YACvB,IAAI,EAAE,SAAS;SAChB;QACD,KAAK,EAAE;YACL,OAAO,EAAE,gEAAgE;YACzE,MAAM,EAAE,eAAe;YACvB,IAAI,EAAE,SAAS;SAChB;QACD,MAAM,EAAE;YACN,OAAO,EAAE,6EAA6E;YACtF,MAAM,EAAE,eAAe;YACvB,IAAI,EAAE,SAAS;SAChB;KACF;IAED,IAAI,EAAE;QACJ,SAAS,EAAE,EAAE,GAAG,EAAE,uBAAuB,EAAE,SAAS,EAAE,sBAAsB,EAAE;QAC9E,SAAS,EAAE,EAAE,GAAG,EAAE,wBAAwB,EAAE,SAAS,EAAE,sBAAsB,EAAE;QAC/E,MAAM,EAAE,0EAA0E;QAClF,IAAI,EAAE,SAAS;KAChB;IAED,WAAW,EAAE;QACX,OAAO,EACL,iFAAiF;YACjF,mFAAmF;QACrF,MAAM,EAAE,iBAAiB;QACzB,IAAI,EAAE,SAAS;KAChB;IAED,kBAAkB,EAAE;QAClB,cAAc,EAAE,IAAI;QACpB,aAAa,EAAE,IAAI;QACnB,MAAM,EAAE,kEAAkE;QAC1E,IAAI,EAAE,SAAS;KAChB;IAED,YAAY,EAAE;QACZ,cAAc,EAAE,IAAI;QACpB,aAAa,EAAE,IAAI;QACnB,MAAM,EACJ,2FAA2F;QAC7F,IAAI,EAAE,SAAS;KAChB;IAED,QAAQ,EAAE;QACR;YACE,OAAO,EACL,yFAAyF;gBACzF,sDAAsD;YACxD,MAAM,EAAE,0BAA0B;YAClC,IAAI,EAAE,SAAS;SAChB;QACD;YACE,OAAO,EAAE,+CAA+C;YACxD,MAAM,EAAE,qBAAqB;YAC7B,IAAI,EAAE,SAAS;SAChB;QACD;YACE,OAAO,EAAE,oEAAoE;YAC7E,MAAM,EAAE,oCAAoC;YAC5C,IAAI,EAAE,SAAS;SAChB;KACF;IAED,OAAO,EAAE;QACP;YACE,OAAO,EAAE,iEAAiE;YAC1E,MAAM,EAAE,iCAAiC;YACzC,IAAI,EAAE,SAAS;SAChB;QACD;YACE,OAAO,EAAE,uEAAuE;YAChF,MAAM,EAAE,iCAAiC;YACzC,IAAI,EAAE,SAAS;SAChB;KACF;CACF,CAAC;AAYF;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CACnC,GAAS,EACT,YAA0B,aAAa;IAEvC,MAAM,UAAU,GAAG,UAAU,CAAC;IAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,SAAS,CAAC,UAAU,YAAY,CAAC,CAAC;IAC7D,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,EAAE,GAAG,CAAC,WAAW,EAAE,EAAE,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC;IAClF,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,GAAG,UAAU,CAAC,CAAC;IAC1D,OAAO,EAAE,GAAG,EAAE,SAAS,IAAI,CAAC,EAAE,UAAU,EAAE,SAAS,CAAC,UAAU,EAAE,SAAS,EAAE,CAAC;AAC9E,CAAC","sourcesContent":["/**\n * Single source of truth for the post-quantum standards this tool depends on.\n *\n * The scanner's credibility rests on its recommendations tracking the current\n * NIST / CNSA / IETF state. That tracking used to be ad-hoc — facts were spread\n * across `remediation.ts` with no dates, no citations, and nothing to catch code\n * drifting from the published standards. This module makes the standards facts\n * explicit, dated, and cited, and the companion drift test\n * (`test/standards.test.ts`) fails the build if the runtime constants\n * (`TIER_PARAMS`, `PQC_TRANSITION_NOTE`, `STATEFUL_HBS_NOTE`) fall out of sync\n * with what is recorded here.\n *\n * ## Cadence\n *\n * Re-verify every quarter (see `docs/standards/pqc-standards.md` for the runbook).\n * On each review: check the sources below for changes, update the facts + their\n * `asOf`, and roll `lastReviewed` / `nextReview` forward. `scripts/standards-check.mjs`\n * (advisory, runs in CI) flags when `nextReview` has passed so a review can't be\n * silently skipped. `standardsReviewStatus(now)` is the pure predicate behind it.\n */\n\n/** A single standards fact with its citation and when it was last verified. */\nexport interface StandardsCitation {\n /** One-line statement of the fact. */\n readonly summary: string;\n /** Spec identifier / publication (and URL where stable). */\n readonly source: string;\n /** `YYYY-MM` — when this fact was last verified against its source. */\n readonly asOf: string;\n}\n\n/** The full post-quantum standards snapshot the tool tracks. */\nexport interface PqcStandards {\n /** `YYYY-MM-DD` — when the whole snapshot was last reviewed. */\n readonly lastReviewed: string;\n /** `YYYY-MM-DD` — when the next review is due. */\n readonly nextReview: string;\n /** Cadence, in months, between reviews. */\n readonly reviewIntervalMonths: number;\n\n /** NIST's finalized PQC FIPS (the recommendation targets). */\n readonly fips: {\n readonly mlKem: StandardsCitation; // FIPS 203\n readonly mlDsa: StandardsCitation; // FIPS 204\n readonly slhDsa: StandardsCitation; // FIPS 205\n };\n\n /**\n * CNSA 2.0 security tiers → the KEM / signature parameter sets. These MUST\n * mirror `remediation.TIER_PARAMS`; the drift test asserts they stay identical.\n */\n readonly cnsa: {\n readonly category3: { readonly kem: string; readonly signature: string };\n readonly category5: { readonly kem: string; readonly signature: string };\n readonly source: string;\n readonly asOf: string;\n };\n\n /** Stateful hash-based signatures (firmware / boot signing). */\n readonly statefulHbs: StandardsCitation; // SP 800-208\n\n /** The NIST IR 8547 migration deadline the transition note surfaces. */\n readonly transitionTimeline: {\n /** Year after which classical public-key crypto is deprecated. */\n readonly deprecateAfter: number;\n /** Year after which it is disallowed. */\n readonly disallowAfter: number;\n readonly source: string;\n readonly asOf: string;\n };\n\n /**\n * CNSA 2.0's OWN migration milestones — distinct from the IR 8547 timeline\n * above. CNSA 2.0 sets exclusive-use dates per system class; the `cnsa-2.0`\n * mandate encodes the earliest hard milestone as `deprecate` and the general\n * exclusive-use milestone as `disallow`. Kept separate so the two mandates\n * ({@link MANDATES}) each derive from their own dated source.\n */\n readonly cnsaTimeline: {\n /** Year after which classical PKC is deprecated (2030: software/firmware signing exclusive). */\n readonly deprecateAfter: number;\n /** Year after which it is disallowed (2033: general NSS exclusive use). */\n readonly disallowAfter: number;\n readonly source: string;\n readonly asOf: string;\n };\n\n /** Emerging / backup standards worth tracking beyond the current FIPS. */\n readonly emerging: readonly StandardsCitation[];\n\n /** Recommended hybrid key-exchange groups. */\n readonly hybrids: readonly StandardsCitation[];\n}\n\n/**\n * The current snapshot. Update on each quarterly review; the drift test keeps the\n * runtime remediation constants aligned with it.\n */\nexport const PQC_STANDARDS: PqcStandards = {\n lastReviewed: \"2026-07-19\",\n nextReview: \"2026-10-19\",\n reviewIntervalMonths: 3,\n\n fips: {\n mlKem: {\n summary: \"ML-KEM (Kyber) key encapsulation — finalized August 2024.\",\n source: \"NIST FIPS 203\",\n asOf: \"2026-07\",\n },\n mlDsa: {\n summary: \"ML-DSA (Dilithium) lattice signatures — finalized August 2024.\",\n source: \"NIST FIPS 204\",\n asOf: \"2026-07\",\n },\n slhDsa: {\n summary: \"SLH-DSA (SPHINCS+) stateless hash-based signatures — finalized August 2024.\",\n source: \"NIST FIPS 205\",\n asOf: \"2026-07\",\n },\n },\n\n cnsa: {\n category3: { kem: \"ML-KEM-768 (FIPS 203)\", signature: \"ML-DSA-65 (FIPS 204)\" },\n category5: { kem: \"ML-KEM-1024 (FIPS 203)\", signature: \"ML-DSA-87 (FIPS 204)\" },\n source: \"NSA CNSA 2.0 (national-security systems; 2030/2033 migration milestones)\",\n asOf: \"2026-07\",\n },\n\n statefulHbs: {\n summary:\n \"LMS/HSS and XMSS/XMSSMT stateful hash-based signatures (incl. the SHAKE256 and \" +\n \"192-bit parameter sets) are approved for firmware/boot signing, but are STATEFUL.\",\n source: \"NIST SP 800-208\",\n asOf: \"2026-07\",\n },\n\n transitionTimeline: {\n deprecateAfter: 2030,\n disallowAfter: 2035,\n source: \"NIST IR 8547 (transition to post-quantum cryptography standards)\",\n asOf: \"2026-07\",\n },\n\n cnsaTimeline: {\n deprecateAfter: 2030,\n disallowAfter: 2033,\n source:\n \"NSA CNSA 2.0 (exclusive-use milestones: 2030 software/firmware signing, 2033 general NSS)\",\n asOf: \"2026-07\",\n },\n\n emerging: [\n {\n summary:\n \"HQC — NIST's code-based backup KEM (selected March 2025; draft FIPS expected ~2026), a \" +\n \"diversity hedge against ML-KEM's lattice assumption.\",\n source: \"NIST PQC (HQC selection)\",\n asOf: \"2026-07\",\n },\n {\n summary: \"FN-DSA / Falcon — compact lattice signatures.\",\n source: \"NIST draft FIPS 206\",\n asOf: \"2026-07\",\n },\n {\n summary: \"X-Wing — X25519 + ML-KEM-768 hybrid KEM for HPKE-style encryption.\",\n source: \"IETF draft-connolly-cfrg-xwing-kem\",\n asOf: \"2026-07\",\n },\n ],\n\n hybrids: [\n {\n summary: \"X25519MLKEM768 — the default TLS 1.3 hybrid key-exchange group.\",\n source: \"IETF draft-ietf-tls-ecdhe-mlkem\",\n asOf: \"2026-07\",\n },\n {\n summary: \"SecP384r1MLKEM1024 — the Category-5 / CNSA hybrid key-exchange group.\",\n source: \"IETF draft-ietf-tls-ecdhe-mlkem\",\n asOf: \"2026-07\",\n },\n ],\n};\n\n/** Result of {@link standardsReviewStatus}. */\nexport interface StandardsReviewStatus {\n /** True when `now` is on or after `nextReview` — a review is due. */\n readonly due: boolean;\n /** The `nextReview` date being compared against (`YYYY-MM-DD`). */\n readonly nextReview: string;\n /** Whole days from `now` until `nextReview` (negative when overdue). */\n readonly daysUntil: number;\n}\n\n/**\n * Whether a standards review is due as of `now`. Pure (takes `now` explicitly) so\n * it is deterministic in tests; the CI script passes the real clock. Compares on\n * whole UTC days so a same-day run is not spuriously \"overdue\".\n */\nexport function standardsReviewStatus(\n now: Date,\n standards: PqcStandards = PQC_STANDARDS,\n): StandardsReviewStatus {\n const MS_PER_DAY = 86_400_000;\n const next = Date.parse(`${standards.nextReview}T00:00:00Z`);\n const today = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());\n const daysUntil = Math.round((next - today) / MS_PER_DAY);\n return { due: daysUntil <= 0, nextReview: standards.nextReview, daysUntil };\n}\n"]} |
@@ -6,3 +6,3 @@ /** | ||
| */ | ||
| export declare const VERSION = "0.8.0"; | ||
| export declare const VERSION = "0.9.0"; | ||
| //# sourceMappingURL=version.d.ts.map |
+1
-1
@@ -6,3 +6,3 @@ /** | ||
| */ | ||
| export const VERSION = "0.8.0"; | ||
| export const VERSION = "0.9.0"; | ||
| //# sourceMappingURL=version.js.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC","sourcesContent":["/**\n * The tool version surfaced in reports. Kept in its own module so reporters and\n * the scan orchestrator can import it without creating a cycle through index.ts.\n * Keep in sync with packages/core/package.json.\n */\nexport const VERSION = \"0.8.0\";\n"]} | ||
| {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC","sourcesContent":["/**\n * The tool version surfaced in reports. Kept in its own module so reporters and\n * the scan orchestrator can import it without creating a cycle through index.ts.\n * Keep in sync with packages/core/package.json.\n */\nexport const VERSION = \"0.9.0\";\n"]} |
+1
-1
| { | ||
| "name": "@quantakrypto/core", | ||
| "version": "0.8.0", | ||
| "version": "0.9.0", | ||
| "description": "Shared post-quantum readiness library: crypto detectors, vulnerable-dependency database, inventory + SARIF reporting. Zero runtime dependencies.", | ||
@@ -5,0 +5,0 @@ "license": "Apache-2.0", |
+27
-0
@@ -26,2 +26,3 @@ /** | ||
| import type { CryptoPolicy, PolicyMapping } from "./policy.js"; | ||
| import type { MandateEvaluation } from "./mandates.js"; | ||
@@ -52,2 +53,10 @@ /** Stable per-finding record for the evidence body (deterministic per commit). */ | ||
| policyMapping?: PolicyMapping; | ||
| /** | ||
| * Compliance-mandate verdicts (`--mandate`), present only when mandates were | ||
| * evaluated. DATE-PINNED for reproducibility: its `now` is stored as a plain | ||
| * `YYYY-MM-DD` (not the volatile scan timestamp) so the same scan of the same | ||
| * commit ON THE SAME DAY yields the same attestation hash, while a genuinely | ||
| * different compliance date (a passed deadline) correctly changes it. | ||
| */ | ||
| mandateMapping?: MandateEvaluation; | ||
| cbom: unknown; | ||
@@ -95,2 +104,8 @@ attestation: { | ||
| policy?: CryptoPolicy; | ||
| /** | ||
| * Optional compliance-mandate evaluation ({@link evaluateMandates}) — adds the | ||
| * `mandateMapping` block. Date-pinned into the hashed body (see | ||
| * {@link ReadinessReport.mandateMapping}). | ||
| */ | ||
| mandate?: MandateEvaluation; | ||
| } | ||
@@ -124,2 +139,12 @@ | ||
| // Compliance mandates: attest the dated verdicts too. The evaluation is a pure | ||
| // function of (findings, date), and the findings are already hashed — so we | ||
| // DATE-PIN its `now` to a plain `YYYY-MM-DD` (dropping the volatile scan | ||
| // timestamp) and hash that. Two runs on the same commit ON THE SAME DAY then | ||
| // reproduce; a run after a deadline has passed correctly attests a different | ||
| // status (and a different hash). | ||
| const mandateMapping = opts.mandate | ||
| ? { ...opts.mandate, now: opts.mandate.now.slice(0, 10) } | ||
| : undefined; | ||
| const hashableBody = { | ||
@@ -137,2 +162,3 @@ reportType: "quantakrypto-readiness", | ||
| ...(policyMapping ? { policyMapping } : {}), | ||
| ...(mandateMapping ? { mandateMapping } : {}), | ||
| }; | ||
@@ -195,2 +221,3 @@ const contentHash = | ||
| ...(report.policyMapping ? { policyMapping: report.policyMapping } : {}), | ||
| ...(report.mandateMapping ? { mandateMapping: report.mandateMapping } : {}), | ||
| }; | ||
@@ -197,0 +224,0 @@ const computedHash = |
+115
-23
@@ -6,3 +6,3 @@ /** | ||
| * date-blind. A mandate adds the missing dimension: named clauses with an effective | ||
| * DATE ("CNSA 2.0 disallows classical public-key crypto after 2035"). The evaluator | ||
| * DATE ("CNSA 2.0 disallows classical public-key crypto after 2033"). The evaluator | ||
| * compares each finding's algorithm against the selected mandates and today's date, | ||
@@ -25,2 +25,4 @@ * so a finding on a prohibited family reads as `due` (every deadline still ahead), | ||
| import { PQC_STANDARDS } from "./standards.js"; | ||
| import { verdictForAlgorithm } from "./policy.js"; | ||
| import type { CryptoPolicy, PolicyVerdict } from "./policy.js"; | ||
@@ -59,5 +61,8 @@ /** | ||
| /** | ||
| * Effective dates derived from the standards source of truth | ||
| * (`PQC_STANDARDS.transitionTimeline`), so a quarterly standards update moves the | ||
| * mandate deadlines automatically (test/standards.test.ts asserts they agree). | ||
| * Effective dates derived from the standards source of truth, so a quarterly | ||
| * standards update moves the mandate deadlines automatically (test/standards.test.ts | ||
| * asserts they agree). Each regime uses its OWN dated timeline: NIST IR 8547 | ||
| * disallows after 2035, while CNSA 2.0 sets its general exclusive-use milestone | ||
| * at 2033 (both deprecate after 2030) — so the two mandates carry different | ||
| * disallow years rather than sharing one. | ||
| * | ||
@@ -69,5 +74,8 @@ * Boundary choice: "deprecate AFTER 2030" leaves the whole stated year permitted, | ||
| */ | ||
| const { deprecateAfter, disallowAfter } = PQC_STANDARDS.transitionTimeline; | ||
| const DEPRECATE_EFFECTIVE = `${deprecateAfter}-12-31`; | ||
| const DISALLOW_EFFECTIVE = `${disallowAfter}-12-31`; | ||
| const IR8547 = PQC_STANDARDS.transitionTimeline; // 2030 deprecate / 2035 disallow | ||
| const CNSA = PQC_STANDARDS.cnsaTimeline; // 2030 deprecate / 2033 disallow | ||
| const NIST_DEPRECATE_EFFECTIVE = `${IR8547.deprecateAfter}-12-31`; | ||
| const NIST_DISALLOW_EFFECTIVE = `${IR8547.disallowAfter}-12-31`; | ||
| const CNSA_DEPRECATE_EFFECTIVE = `${CNSA.deprecateAfter}-12-31`; | ||
| const CNSA_DISALLOW_EFFECTIVE = `${CNSA.disallowAfter}-12-31`; | ||
@@ -110,14 +118,14 @@ /** Which enforcement tier a clause encodes: warn (`deprecate`) or fail (`disallow`). */ | ||
| { | ||
| clause: `CNSA 2.0 — deprecate classical PKC after ${deprecateAfter}`, | ||
| clause: `CNSA 2.0 — deprecate classical PKC after ${CNSA.deprecateAfter}`, | ||
| tier: "deprecate", | ||
| prohibits: [...PROHIBITED_FAMILIES], | ||
| effective: DEPRECATE_EFFECTIVE, | ||
| note: "Classical public-key cryptography deprecated; systems should use CNSA 2.0 PQC exclusively.", | ||
| effective: CNSA_DEPRECATE_EFFECTIVE, | ||
| note: `Classical public-key cryptography deprecated (${CNSA.deprecateAfter}: software/firmware signing exclusive-use); systems should use CNSA 2.0 PQC.`, | ||
| }, | ||
| { | ||
| clause: `CNSA 2.0 — disallow classical PKC after ${disallowAfter}`, | ||
| clause: `CNSA 2.0 — disallow classical PKC after ${CNSA.disallowAfter}`, | ||
| tier: "disallow", | ||
| prohibits: [...PROHIBITED_FAMILIES], | ||
| effective: DISALLOW_EFFECTIVE, | ||
| note: "Classical public-key cryptography disallowed; the migration must be complete.", | ||
| effective: CNSA_DISALLOW_EFFECTIVE, | ||
| note: `Classical public-key cryptography disallowed (${CNSA.disallowAfter}: general NSS exclusive-use milestone); the migration must be complete.`, | ||
| }, | ||
@@ -134,14 +142,14 @@ ], | ||
| { | ||
| clause: `NIST IR 8547 — deprecate classical PKC after ${deprecateAfter}`, | ||
| clause: `NIST IR 8547 — deprecate classical PKC after ${IR8547.deprecateAfter}`, | ||
| tier: "deprecate", | ||
| prohibits: [...PROHIBITED_FAMILIES], | ||
| effective: DEPRECATE_EFFECTIVE, | ||
| note: `112-bit-security classical public-key algorithms deprecated after ${deprecateAfter}.`, | ||
| effective: NIST_DEPRECATE_EFFECTIVE, | ||
| note: `112-bit-security classical public-key algorithms deprecated after ${IR8547.deprecateAfter}.`, | ||
| }, | ||
| { | ||
| clause: `NIST IR 8547 — disallow classical PKC after ${disallowAfter}`, | ||
| clause: `NIST IR 8547 — disallow classical PKC after ${IR8547.disallowAfter}`, | ||
| tier: "disallow", | ||
| prohibits: [...PROHIBITED_FAMILIES], | ||
| effective: DISALLOW_EFFECTIVE, | ||
| note: `Classical public-key algorithms disallowed after ${disallowAfter}.`, | ||
| effective: NIST_DISALLOW_EFFECTIVE, | ||
| note: `Classical public-key algorithms disallowed after ${IR8547.disallowAfter}.`, | ||
| }, | ||
@@ -206,2 +214,17 @@ ], | ||
| citation: string; | ||
| /** | ||
| * The org cryptography policy's verdict on this algorithm family when a policy | ||
| * was composed in via {@link evaluateMandates}' `policy` argument, else null. | ||
| * Purely informational — it records the org's own stance next to the mandate's | ||
| * dated clause so a machine-readable report shows both. | ||
| */ | ||
| policyVerdict: PolicyVerdict | null; | ||
| /** | ||
| * True when the org policy EXPLICITLY permits or is transitioning this family — | ||
| * an owned, tracked decision. Acknowledged findings are exempt from the EARLY | ||
| * gates (`failNow` / `leadMonths`); a passed DISALLOW deadline (`violation`) | ||
| * still fails regardless, because an org cannot self-exempt from a dated legal | ||
| * disallow. `false` when no policy was supplied. | ||
| */ | ||
| acknowledged: boolean; | ||
| } | ||
@@ -231,2 +254,11 @@ | ||
| hasViolation: boolean; | ||
| /** Name of the org policy composed in via `policy`, or null when none was supplied. */ | ||
| policyName: string | null; | ||
| /** | ||
| * How many distinct prohibited FINDINGS the org policy explicitly acknowledged | ||
| * (family listed as `permitted` or `inTransition`) — counted per finding, not | ||
| * per verdict row, so a family prohibited by two mandates counts once, matching | ||
| * the per-finding `summary`. 0 when no policy was supplied. | ||
| */ | ||
| acknowledged: number; | ||
| } | ||
@@ -248,2 +280,20 @@ | ||
| /** | ||
| * True when the org policy EXPLICITLY accepts a family — listed in `permitted` | ||
| * (an owned exception) or `inTransition` (a tracked migration). A `prohibited` | ||
| * family or one covered only by the policy's default fallback is NOT | ||
| * acknowledged: silence is not consent, so an unnamed family never earns a gate | ||
| * exemption. | ||
| * | ||
| * `prohibited` takes precedence, matching {@link verdictForAlgorithm}: a policy | ||
| * that lists a family in BOTH `prohibited` and `permitted` (a plausible merge of | ||
| * two policy fragments) resolves to `violation`, and must not then be silently | ||
| * acknowledged away — that would produce a self-contradictory verdict (verdict | ||
| * `violation`, yet exempt from the gate). | ||
| */ | ||
| function policyAcknowledges(algo: AlgorithmFamily, policy: CryptoPolicy): boolean { | ||
| if (policy.prohibited?.includes(algo)) return false; | ||
| return Boolean(policy.permitted?.includes(algo) || policy.inTransition?.includes(algo)); | ||
| } | ||
| /** | ||
| * Evaluate findings against the selected mandates as of `now`. Unknown mandate | ||
@@ -254,2 +304,9 @@ * ids are ignored — callers validate up front with {@link assertKnownMandates}. | ||
| * contributes a verdict row. | ||
| * | ||
| * When an org `policy` is supplied (the `--policy` composition), every verdict | ||
| * row is annotated with the org's own `policyVerdict` and an `acknowledged` flag | ||
| * (family explicitly permitted / in-transition). Acknowledgement is purely | ||
| * additive here — it changes no status — but {@link mandateGateFails} honours it | ||
| * to keep the early gates from double-flagging crypto the org is knowingly, | ||
| * traceably managing. A passed DISALLOW deadline is never acknowledgeable away. | ||
| */ | ||
@@ -260,4 +317,14 @@ export function evaluateMandates( | ||
| now: Date, | ||
| policy?: CryptoPolicy, | ||
| ): MandateEvaluation { | ||
| const nowMs = now.getTime(); | ||
| // A compliance verdict is as-of a DAY: the clauses take effect on date | ||
| // boundaries (YYYY-MM-DD), so the exact clock time carries no compliance | ||
| // meaning. Pin `now` to UTC midnight of its date before any arithmetic — this | ||
| // makes the whole evaluation (statuses AND the monthsUntil / monthsUntilDisallow | ||
| // counters) identical for any two runs on the same day, which is what keeps the | ||
| // attested evidence hash reproducible per commit per day. Truncating changes no | ||
| // status: every `effective` date is itself UTC-midnight, so `nowMs >= effMs` has | ||
| // the same truth value at midnight as at any other time that day. | ||
| const nowMs = Date.parse(`${now.toISOString().slice(0, 10)}T00:00:00.000Z`); | ||
| const nowIso = new Date(nowMs).toISOString(); | ||
| const selected = mandateIdList.map(getMandate).filter((m): m is Mandate => Boolean(m)); | ||
@@ -268,2 +335,3 @@ | ||
| let notInScope = 0; | ||
| let acknowledged = 0; | ||
| let nextDeadlineMs: number | null = null; | ||
@@ -278,2 +346,9 @@ | ||
| let worst: MandateStatus = "conformant"; | ||
| // Acknowledgement is a property of the FAMILY (fixed for this finding), so it | ||
| // is computed once here and stamped on every row. The tally counts distinct | ||
| // acknowledged findings (not rows), so one family under two mandates is one | ||
| // acknowledgement, matching the per-finding status counts in `summary`. | ||
| const family = algo as AlgorithmFamily; | ||
| const isAcknowledged = policy ? policyAcknowledges(family, policy) : false; | ||
| let producedRow = false; | ||
| for (const mandate of selected) { | ||
@@ -285,2 +360,3 @@ // The applicable clauses for this family, earliest deadline first. | ||
| if (applicable.length === 0) continue; | ||
| producedRow = true; | ||
@@ -331,4 +407,9 @@ // Tier the clauses so both stay live: a passed DISALLOW clause is a | ||
| citation: mandate.citation, | ||
| policyVerdict: policy ? verdictForAlgorithm(family, policy).verdict : null, | ||
| acknowledged: isAcknowledged, | ||
| }); | ||
| } | ||
| // Count the acknowledged FINDING once (it produced at least one prohibited | ||
| // row and the org policy owns/tracks its family), not once per mandate row. | ||
| if (producedRow && isAcknowledged) acknowledged++; | ||
| perFindingWorst.push(worst); | ||
@@ -346,3 +427,3 @@ } | ||
| return { | ||
| now: now.toISOString(), | ||
| now: nowIso, | ||
| mandates: selected.map((m) => m.id), | ||
@@ -355,2 +436,4 @@ summary, | ||
| hasViolation: summary.violation > 0, | ||
| policyName: policy?.name ?? null, | ||
| acknowledged, | ||
| }; | ||
@@ -372,8 +455,17 @@ } | ||
| * immediately. | ||
| * | ||
| * Policy composition: when a finding was `acknowledged` by the org policy | ||
| * (`--policy`), it is exempt from the EARLY gates (`failNow` / `leadMonths`) — | ||
| * the org is knowingly, traceably managing that family, so its own early | ||
| * enforcement should not re-flag it. A passed DISALLOW deadline (`violation`) | ||
| * still fails regardless: a dated legal disallow is not something an org can | ||
| * self-exempt from. | ||
| */ | ||
| export function mandateGateFails(ev: MandateEvaluation, opts: MandateGateOptions = {}): boolean { | ||
| if (ev.hasViolation) return true; | ||
| if (opts.failNow) return ev.findings.length > 0; | ||
| // Early gates skip policy-acknowledged findings; the hard `violation` above did not. | ||
| const gated = ev.findings.filter((v) => !v.acknowledged); | ||
| if (opts.failNow) return gated.length > 0; | ||
| if (opts.leadMonths !== undefined) { | ||
| return ev.findings.some( | ||
| return gated.some( | ||
| (v) => v.monthsUntilDisallow !== null && v.monthsUntilDisallow <= opts.leadMonths!, | ||
@@ -380,0 +472,0 @@ ); |
+22
-1
@@ -16,2 +16,3 @@ /** | ||
| import type { FindingExposure, HndlReport } from "./hndl.js"; | ||
| import type { MandateEvaluation } from "./mandates.js"; | ||
@@ -49,2 +50,10 @@ /** Minimal SARIF 2.1.0 log shape (kept permissive on purpose). */ | ||
| hndl?: HndlReport; | ||
| /** | ||
| * Optional compliance-mandate evaluation ({@link evaluateMandates}). When | ||
| * supplied, the JSON report carries a top-level `mandateMapping` block and the | ||
| * SARIF run carries the same under `run.properties.mandate` — the | ||
| * machine-readable half of the `--mandate` gate for CI consumption. Purely | ||
| * additive: it never changes finding identity, ordering, or exit codes. | ||
| */ | ||
| mandate?: MandateEvaluation; | ||
| } | ||
@@ -264,2 +273,9 @@ | ||
| // Run-level properties bag: the repo HNDL summary and/or the compliance-mandate | ||
| // evaluation, whichever were supplied. Both are additive metadata for SARIF | ||
| // consumers (our platform ingest, CI) and never affect result identity. | ||
| const runProperties: Record<string, unknown> = {}; | ||
| if (opts?.hndl) runProperties.hndl = hndlSummaryBlock(opts.hndl); | ||
| if (opts?.mandate) runProperties.mandate = opts.mandate; | ||
| return { | ||
@@ -279,3 +295,3 @@ $schema: SARIF_SCHEMA, | ||
| ...(taxonomies.length > 0 ? { taxonomies } : {}), | ||
| ...(opts?.hndl ? { properties: { hndl: hndlSummaryBlock(opts.hndl) } } : {}), | ||
| ...(Object.keys(runProperties).length > 0 ? { properties: runProperties } : {}), | ||
| results, | ||
@@ -323,2 +339,7 @@ }, | ||
| ...(hndl ? { hndl: hndlSummaryBlock(hndl) } : {}), | ||
| // Compliance-mandate evaluation (`--mandate`): the machine-readable verdicts | ||
| // + summary, so a CI job can gate/report on them without re-parsing the human | ||
| // block. Carries the org `--policy` composition (policyVerdict / acknowledged) | ||
| // when one was supplied. | ||
| ...(opts?.mandate ? { mandateMapping: opts.mandate } : {}), | ||
| findings: result.findings.map((f) => { | ||
@@ -325,0 +346,0 @@ const exposure = exposureFor(f, hndl); |
@@ -17,2 +17,4 @@ /** | ||
| import { PQC_STANDARDS } from "./standards.js"; | ||
| /** Whether classical+PQC hybridization is required during the transition, per regime. */ | ||
@@ -62,4 +64,6 @@ export type HybridStance = "required" | "recommended" | "optional"; | ||
| "Hybrid key establishment (e.g. X25519MLKEM768) is permitted and recommended during the transition; pure ML-KEM is also acceptable (SP 800-227 / IR 8547).", | ||
| deprecateAfter: 2030, | ||
| disallowAfter: 2035, | ||
| // Derived from the single source of truth so the profile can never drift from | ||
| // the `nist-ir-8547` mandate gate (the standards drift test asserts this). | ||
| deprecateAfter: PQC_STANDARDS.transitionTimeline.deprecateAfter, | ||
| disallowAfter: PQC_STANDARDS.transitionTimeline.disallowAfter, | ||
| citation: "NIST IR 8547 + FIPS 203/204/205", | ||
@@ -76,5 +80,8 @@ asOf: "2026-07", | ||
| "CNSA 2.0 targets pure PQC and does not require hybrids; if a hybrid TLS group is used, it must be SecP384r1MLKEM1024 — X25519MLKEM768's ML-KEM-768 component is sub-CNSA.", | ||
| deprecateAfter: 2030, | ||
| disallowAfter: 2035, | ||
| citation: "NSA CNSA 2.0 (2030/2033/2035 migration milestones)", | ||
| // CNSA 2.0 carries its OWN exclusive-use milestones (2030 software/firmware | ||
| // signing, 2033 general NSS) — NOT IR 8547's 2035. Derived from the single | ||
| // source so `--profile cnsa-2.0` and `--mandate cnsa-2.0` always agree. | ||
| deprecateAfter: PQC_STANDARDS.cnsaTimeline.deprecateAfter, | ||
| disallowAfter: PQC_STANDARDS.cnsaTimeline.disallowAfter, | ||
| citation: "NSA CNSA 2.0 (2030 deprecate / 2033 disallow exclusive-use milestones)", | ||
| asOf: "2026-07", | ||
@@ -81,0 +88,0 @@ }, |
+25
-1
@@ -62,3 +62,3 @@ /** | ||
| /** The migration deadline the transition note surfaces. */ | ||
| /** The NIST IR 8547 migration deadline the transition note surfaces. */ | ||
| readonly transitionTimeline: { | ||
@@ -73,2 +73,18 @@ /** Year after which classical public-key crypto is deprecated. */ | ||
| /** | ||
| * CNSA 2.0's OWN migration milestones — distinct from the IR 8547 timeline | ||
| * above. CNSA 2.0 sets exclusive-use dates per system class; the `cnsa-2.0` | ||
| * mandate encodes the earliest hard milestone as `deprecate` and the general | ||
| * exclusive-use milestone as `disallow`. Kept separate so the two mandates | ||
| * ({@link MANDATES}) each derive from their own dated source. | ||
| */ | ||
| readonly cnsaTimeline: { | ||
| /** Year after which classical PKC is deprecated (2030: software/firmware signing exclusive). */ | ||
| readonly deprecateAfter: number; | ||
| /** Year after which it is disallowed (2033: general NSS exclusive use). */ | ||
| readonly disallowAfter: number; | ||
| readonly source: string; | ||
| readonly asOf: string; | ||
| }; | ||
| /** Emerging / backup standards worth tracking beyond the current FIPS. */ | ||
@@ -130,2 +146,10 @@ readonly emerging: readonly StandardsCitation[]; | ||
| cnsaTimeline: { | ||
| deprecateAfter: 2030, | ||
| disallowAfter: 2033, | ||
| source: | ||
| "NSA CNSA 2.0 (exclusive-use milestones: 2030 software/firmware signing, 2033 general NSS)", | ||
| asOf: "2026-07", | ||
| }, | ||
| emerging: [ | ||
@@ -132,0 +156,0 @@ { |
+1
-1
@@ -6,2 +6,2 @@ /** | ||
| */ | ||
| export const VERSION = "0.8.0"; | ||
| export const VERSION = "0.9.0"; |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
3144538
1.1%39481
0.88%