@quantakrypto/core
Advanced tools
| import type { Finding, RuleMeta } from "./types.js"; | ||
| /** | ||
| * Generic catalog entry for the `dep-advisory` rule. The per-advisory specifics | ||
| * (title / severity / patched version) live on each individual finding; this is | ||
| * the shared, package-agnostic description SARIF advertises for the rule. | ||
| */ | ||
| export declare const DEP_ADVISORY_RULE: RuleMeta; | ||
| /** Options for {@link scanAdvisories}. */ | ||
| export interface ScanAdvisoriesOptions { | ||
| /** Per-tool timeout in milliseconds. Default: 120_000. */ | ||
| timeoutMs?: number; | ||
| /** Max stdout buffer per tool, in bytes. Default: 32 MiB. */ | ||
| maxBuffer?: number; | ||
| /** | ||
| * Injectable command runner (for tests). Resolves with the tool's stdout, or | ||
| * rejects with an error carrying `code` (e.g. `"ENOENT"`) and, for a non-zero | ||
| * exit, the captured `stdout`. Defaults to a promisified `execFile`. | ||
| */ | ||
| exec?: ExecFn; | ||
| /** Injectable directory lister (for tests). Defaults to `fs.readdir`. */ | ||
| listDir?: (dir: string) => Promise<string[]>; | ||
| } | ||
| /** Shape of an injectable command runner and of the errors it may reject with. */ | ||
| export type ExecFn = (command: string, args: readonly string[], options: { | ||
| cwd: string; | ||
| timeout: number; | ||
| maxBuffer: number; | ||
| }) => Promise<{ | ||
| stdout: string; | ||
| stderr: string; | ||
| }>; | ||
| /** | ||
| * Scan `root` for dependency security advisories by shelling out to each present | ||
| * ecosystem's audit tool. Never throws: a missing tool or a tool error becomes a | ||
| * diagnostic string. Returns the merged findings plus the diagnostics. | ||
| */ | ||
| export declare function scanAdvisories(root: string, opts?: ScanAdvisoriesOptions): Promise<{ | ||
| findings: Finding[]; | ||
| diagnostics: string[]; | ||
| }>; | ||
| //# sourceMappingURL=advisories.d.ts.map |
| {"version":3,"file":"advisories.d.ts","sourceRoot":"","sources":["../src/advisories.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAY,MAAM,YAAY,CAAC;AAI9D;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,EAAE,QAa/B,CAAC;AAEF,0CAA0C;AAC1C,MAAM,WAAW,qBAAqB;IACpC,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CAC9C;AAED,kFAAkF;AAClF,MAAM,MAAM,MAAM,GAAG,CACnB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,KACzD,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AA2NjD;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,qBAA0B,GAC/B,OAAO,CAAC;IAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;IAAC,WAAW,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CA6EzD"} |
| /** | ||
| * Dependency-advisory scanning (opt-in, wired to `qscan --audit`). | ||
| * | ||
| * Unlike the built-in {@link vulnerableDependencies} database — which flags | ||
| * packages whose *purpose* is quantum-vulnerable classical crypto — this module | ||
| * surfaces KNOWN SECURITY ADVISORIES (CVE / RUSTSEC / GHSA / PYSEC) against the | ||
| * pinned versions in a project's lockfiles, by shelling out to each ecosystem's | ||
| * own audit tool: | ||
| * - Rust (`Cargo.toml` / `Cargo.lock`) → `cargo audit --json` | ||
| * - Python(`requirements*.txt` / `pyproject.toml`)→ `pip-audit --format json` | ||
| * - npm (`package-lock.json`) → `npm audit --json` | ||
| * | ||
| * DESIGN (mirrors `changed.ts` / `sign.ts`, the blessed shell-out pattern): | ||
| * - `execFile` (never a shell), a timeout, and a bounded `maxBuffer`, all inside | ||
| * a try/catch. A missing tool (`ENOENT`) or any other failure NEVER throws — | ||
| * it degrades to a diagnostic string ("cargo audit not available, skipped"). | ||
| * - The audit tools exit NON-ZERO when they find advisories, so their JSON | ||
| * arrives on the error's `stdout`; that is the normal, expected path. | ||
| * - Zero runtime dependencies (ADR-0001): only `node:child_process` / | ||
| * `node:util` / `node:fs` — the same built-ins `changed.ts` already uses. | ||
| * | ||
| * Findings are `category: "dependency"`, `ruleId: "dep-advisory"`, located at the | ||
| * project's manifest file. They are produced by this scanner, not a registered | ||
| * {@link Detector}, so — exactly like `DEP_VULNERABLE_RULE` — the generic | ||
| * {@link DEP_ADVISORY_RULE} catalog entry is merged into the SARIF `rules[]` by | ||
| * the reporter (see qscan `report.ts`). | ||
| */ | ||
| import { execFile } from "node:child_process"; | ||
| import { promisify } from "node:util"; | ||
| import { readdir } from "node:fs/promises"; | ||
| import * as path from "node:path"; | ||
| const execFileAsync = promisify(execFile); | ||
| /** | ||
| * Generic catalog entry for the `dep-advisory` rule. The per-advisory specifics | ||
| * (title / severity / patched version) live on each individual finding; this is | ||
| * the shared, package-agnostic description SARIF advertises for the rule. | ||
| */ | ||
| export const DEP_ADVISORY_RULE = { | ||
| id: "dep-advisory", | ||
| title: "Dependency with a known security advisory", | ||
| category: "dependency", | ||
| // Representative default; each finding carries its own advisory severity. | ||
| severity: "high", | ||
| confidence: "high", | ||
| hndl: false, | ||
| message: "A pinned dependency has a published security advisory (CVE / RUSTSEC / GHSA / PYSEC). Upgrade to a patched version.", | ||
| remediation: "Upgrade the affected package to the advisory's patched release.", | ||
| description: "Flags dependencies with a known security advisory, via the ecosystem's own audit tool (cargo audit / pip-audit / npm audit). Opt-in with `qscan --audit`.", | ||
| }; | ||
| const DEFAULT_TIMEOUT_MS = 120_000; | ||
| const DEFAULT_MAX_BUFFER = 32 * 1024 * 1024; | ||
| /** Map an ecosystem-reported severity token to our {@link Severity}. */ | ||
| function toSeverity(raw) { | ||
| const s = String(raw ?? "").toLowerCase(); | ||
| if (s === "critical") | ||
| return "critical"; | ||
| if (s === "high") | ||
| return "high"; | ||
| if (s === "moderate" || s === "medium") | ||
| return "medium"; | ||
| if (s === "low") | ||
| return "low"; | ||
| if (s === "info" || s === "informational" || s === "none" || s === "negligible") | ||
| return "info"; | ||
| // An advisory with no usable severity is treated as high (conservative — a | ||
| // known-vulnerable pinned dependency should not silently pass a scan). | ||
| return "high"; | ||
| } | ||
| function asRecord(v) { | ||
| return v !== null && typeof v === "object" ? v : null; | ||
| } | ||
| function str(v) { | ||
| return typeof v === "string" ? v : v === undefined || v === null ? "" : String(v); | ||
| } | ||
| function firstString(v) { | ||
| if (typeof v === "string" && v) | ||
| return v; | ||
| if (Array.isArray(v)) { | ||
| const s = v.find((x) => typeof x === "string" && x); | ||
| return typeof s === "string" ? s : undefined; | ||
| } | ||
| return undefined; | ||
| } | ||
| /** cargo audit --json → advisories. */ | ||
| function parseCargoAudit(json) { | ||
| const root = asRecord(json); | ||
| const vulns = asRecord(root?.vulnerabilities); | ||
| const list = Array.isArray(vulns?.list) ? vulns.list : []; | ||
| const out = []; | ||
| for (const item of list) { | ||
| const rec = asRecord(item); | ||
| const advisory = asRecord(rec?.advisory); | ||
| const pkg = asRecord(rec?.package); | ||
| const versions = asRecord(rec?.versions); | ||
| if (!advisory) | ||
| continue; | ||
| out.push({ | ||
| id: str(advisory.id) || "RUSTSEC-UNKNOWN", | ||
| package: str(pkg?.name) || str(advisory.package), | ||
| version: str(pkg?.version), | ||
| summary: str(advisory.title) || "security advisory", | ||
| severity: toSeverity(advisory.severity), | ||
| patched: firstString(versions?.patched), | ||
| }); | ||
| } | ||
| return out; | ||
| } | ||
| /** pip-audit --format json → advisories. Handles the object + legacy-array forms. */ | ||
| function parsePipAudit(json) { | ||
| const root = asRecord(json); | ||
| const deps = Array.isArray(json) | ||
| ? json | ||
| : Array.isArray(root?.dependencies) | ||
| ? root.dependencies | ||
| : []; | ||
| const out = []; | ||
| for (const item of deps) { | ||
| const dep = asRecord(item); | ||
| if (!dep) | ||
| continue; | ||
| const name = str(dep.name); | ||
| const version = str(dep.version); | ||
| const vulns = Array.isArray(dep.vulns) ? dep.vulns : []; | ||
| for (const v of vulns) { | ||
| const vuln = asRecord(v); | ||
| if (!vuln) | ||
| continue; | ||
| out.push({ | ||
| id: str(vuln.id) || firstString(vuln.aliases) || "PYSEC-UNKNOWN", | ||
| package: name, | ||
| version, | ||
| summary: str(vuln.description) || "security advisory", | ||
| // pip-audit's base JSON does not grade severity; treat as high. | ||
| severity: toSeverity(vuln.severity), | ||
| patched: firstString(vuln.fix_versions), | ||
| }); | ||
| } | ||
| } | ||
| return out; | ||
| } | ||
| /** Extract a GHSA id from an advisory URL when present. */ | ||
| function ghsaFrom(url) { | ||
| const m = /GHSA-[\w-]+/.exec(url); | ||
| return m ? m[0] : undefined; | ||
| } | ||
| /** npm audit --json (npm v7+) → advisories. */ | ||
| function parseNpmAudit(json) { | ||
| const root = asRecord(json); | ||
| const vulns = asRecord(root?.vulnerabilities); | ||
| if (!vulns) | ||
| return []; | ||
| const out = []; | ||
| for (const [name, entry] of Object.entries(vulns)) { | ||
| const rec = asRecord(entry); | ||
| if (!rec) | ||
| continue; | ||
| const range = str(rec.range); | ||
| const fix = asRecord(rec.fixAvailable); | ||
| const patched = fix ? str(fix.version) : undefined; | ||
| const via = Array.isArray(rec.via) ? rec.via : []; | ||
| for (const v of via) { | ||
| const adv = asRecord(v); | ||
| if (!adv) | ||
| continue; // string `via` = transitive; the source entry carries the detail. | ||
| const url = str(adv.url); | ||
| const id = ghsaFrom(url) || | ||
| (adv.source !== undefined ? `npm-advisory-${str(adv.source)}` : url) || | ||
| "npm-advisory"; | ||
| out.push({ | ||
| id, | ||
| package: str(adv.name) || name, | ||
| version: range, | ||
| summary: str(adv.title) || "security advisory", | ||
| severity: toSeverity(adv.severity ?? rec.severity), | ||
| patched: patched || undefined, | ||
| }); | ||
| } | ||
| } | ||
| return out; | ||
| } | ||
| /** Match `requirements*.txt` (requirements.txt, requirements-dev.txt, …). */ | ||
| function isRequirements(name) { | ||
| return /^requirements[\w.-]*\.txt$/i.test(name); | ||
| } | ||
| /** The audit tools, in a deterministic order. */ | ||
| const AUDIT_TOOLS = [ | ||
| { | ||
| label: "cargo audit", | ||
| command: "cargo", | ||
| args: ["audit", "--json"], | ||
| manifest: (e) => e.includes("Cargo.lock") ? "Cargo.lock" : e.includes("Cargo.toml") ? "Cargo.toml" : null, | ||
| parse: parseCargoAudit, | ||
| }, | ||
| { | ||
| label: "pip-audit", | ||
| command: "pip-audit", | ||
| args: ["--format", "json"], | ||
| manifest: (e) => { | ||
| const req = e.find(isRequirements); | ||
| if (req) | ||
| return req; | ||
| return e.includes("pyproject.toml") ? "pyproject.toml" : null; | ||
| }, | ||
| parse: parsePipAudit, | ||
| }, | ||
| { | ||
| label: "npm audit", | ||
| command: "npm", | ||
| args: ["audit", "--json"], | ||
| manifest: (e) => (e.includes("package-lock.json") ? "package-lock.json" : null), | ||
| parse: parseNpmAudit, | ||
| }, | ||
| ]; | ||
| /** Build a {@link Finding} from a normalized advisory record. */ | ||
| function advisoryFinding(rec, manifest) { | ||
| const pkgVer = rec.version ? `${rec.package}@${rec.version}` : rec.package; | ||
| const finding = { | ||
| ruleId: "dep-advisory", | ||
| title: rec.id, | ||
| category: "dependency", | ||
| severity: rec.severity, | ||
| confidence: "high", | ||
| hndl: false, | ||
| message: `${pkgVer}: ${rec.summary} (${rec.id})`, | ||
| location: { file: manifest, line: 1 }, | ||
| }; | ||
| if (rec.patched) | ||
| finding.remediation = `Upgrade ${rec.package} to ${rec.patched}`; | ||
| return finding; | ||
| } | ||
| /** | ||
| * Scan `root` for dependency security advisories by shelling out to each present | ||
| * ecosystem's audit tool. Never throws: a missing tool or a tool error becomes a | ||
| * diagnostic string. Returns the merged findings plus the diagnostics. | ||
| */ | ||
| export async function scanAdvisories(root, opts = {}) { | ||
| const timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS; | ||
| const maxBuffer = opts.maxBuffer ?? DEFAULT_MAX_BUFFER; | ||
| const list = opts.listDir ?? ((dir) => readdir(dir)); | ||
| const exec = opts.exec ?? | ||
| ((command, args, options) => execFileAsync(command, args, { ...options, windowsHide: true })); | ||
| const findings = []; | ||
| const diagnostics = []; | ||
| let entries; | ||
| try { | ||
| entries = await list(root); | ||
| } | ||
| catch { | ||
| return { findings, diagnostics }; // unreadable root — nothing to audit. | ||
| } | ||
| for (const tool of AUDIT_TOOLS) { | ||
| const manifest = tool.manifest(entries); | ||
| if (manifest === null) | ||
| continue; // this ecosystem isn't present. | ||
| let stdout; | ||
| try { | ||
| const res = await exec(tool.command, tool.args, { cwd: root, timeout, maxBuffer }); | ||
| stdout = res.stdout; | ||
| } | ||
| catch (err) { | ||
| const e = err; | ||
| if (e.code === "ENOENT") { | ||
| diagnostics.push(`${tool.label} not available, skipped`); | ||
| continue; | ||
| } | ||
| if (e.killed || e.signal === "SIGTERM") { | ||
| diagnostics.push(`${tool.label} timed out, skipped`); | ||
| continue; | ||
| } | ||
| // A non-zero exit is EXPECTED when advisories are found: the JSON is on | ||
| // the error's stdout. Only when there is no parseable stdout is it a real | ||
| // failure we skip over. | ||
| if (typeof e.stdout === "string" && e.stdout.trim()) { | ||
| stdout = e.stdout; | ||
| } | ||
| else { | ||
| const detail = (e.stderr || e.message || "").trim().slice(0, 160); | ||
| diagnostics.push(`${tool.label} failed, skipped${detail ? `: ${detail}` : ""}`); | ||
| continue; | ||
| } | ||
| } | ||
| let json; | ||
| try { | ||
| json = JSON.parse(stdout); | ||
| } | ||
| catch { | ||
| diagnostics.push(`${tool.label} produced unparseable output, skipped`); | ||
| continue; | ||
| } | ||
| let records; | ||
| try { | ||
| records = tool.parse(json); | ||
| } | ||
| catch { | ||
| diagnostics.push(`${tool.label} output could not be interpreted, skipped`); | ||
| continue; | ||
| } | ||
| // Dedupe by advisory id + package (npm lists the same advisory under both a | ||
| // direct and a transitive path). | ||
| const seen = new Set(); | ||
| for (const rec of records) { | ||
| const key = `${rec.id}|${rec.package}`; | ||
| if (seen.has(key)) | ||
| continue; | ||
| seen.add(key); | ||
| findings.push(advisoryFinding(rec, path.posix.basename(manifest))); | ||
| } | ||
| } | ||
| return { findings, diagnostics }; | ||
| } | ||
| //# sourceMappingURL=advisories.js.map |
| {"version":3,"file":"advisories.js","sourceRoot":"","sources":["../src/advisories.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Dependency-advisory scanning (opt-in, wired to `qscan --audit`).\n *\n * Unlike the built-in {@link vulnerableDependencies} database — which flags\n * packages whose *purpose* is quantum-vulnerable classical crypto — this module\n * surfaces KNOWN SECURITY ADVISORIES (CVE / RUSTSEC / GHSA / PYSEC) against the\n * pinned versions in a project's lockfiles, by shelling out to each ecosystem's\n * own audit tool:\n * - Rust (`Cargo.toml` / `Cargo.lock`) → `cargo audit --json`\n * - Python(`requirements*.txt` / `pyproject.toml`)→ `pip-audit --format json`\n * - npm (`package-lock.json`) → `npm audit --json`\n *\n * DESIGN (mirrors `changed.ts` / `sign.ts`, the blessed shell-out pattern):\n * - `execFile` (never a shell), a timeout, and a bounded `maxBuffer`, all inside\n * a try/catch. A missing tool (`ENOENT`) or any other failure NEVER throws —\n * it degrades to a diagnostic string (\"cargo audit not available, skipped\").\n * - The audit tools exit NON-ZERO when they find advisories, so their JSON\n * arrives on the error's `stdout`; that is the normal, expected path.\n * - Zero runtime dependencies (ADR-0001): only `node:child_process` /\n * `node:util` / `node:fs` — the same built-ins `changed.ts` already uses.\n *\n * Findings are `category: \"dependency\"`, `ruleId: \"dep-advisory\"`, located at the\n * project's manifest file. They are produced by this scanner, not a registered\n * {@link Detector}, so — exactly like `DEP_VULNERABLE_RULE` — the generic\n * {@link DEP_ADVISORY_RULE} catalog entry is merged into the SARIF `rules[]` by\n * the reporter (see qscan `report.ts`).\n */\nimport { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport { readdir } from \"node:fs/promises\";\nimport * as path from \"node:path\";\n\nimport type { Finding, RuleMeta, Severity } from \"./types.js\";\n\nconst execFileAsync = promisify(execFile);\n\n/**\n * Generic catalog entry for the `dep-advisory` rule. The per-advisory specifics\n * (title / severity / patched version) live on each individual finding; this is\n * the shared, package-agnostic description SARIF advertises for the rule.\n */\nexport const DEP_ADVISORY_RULE: RuleMeta = {\n id: \"dep-advisory\",\n title: \"Dependency with a known security advisory\",\n category: \"dependency\",\n // Representative default; each finding carries its own advisory severity.\n severity: \"high\",\n confidence: \"high\",\n hndl: false,\n message:\n \"A pinned dependency has a published security advisory (CVE / RUSTSEC / GHSA / PYSEC). Upgrade to a patched version.\",\n remediation: \"Upgrade the affected package to the advisory's patched release.\",\n description:\n \"Flags dependencies with a known security advisory, via the ecosystem's own audit tool (cargo audit / pip-audit / npm audit). Opt-in with `qscan --audit`.\",\n};\n\n/** Options for {@link scanAdvisories}. */\nexport interface ScanAdvisoriesOptions {\n /** Per-tool timeout in milliseconds. Default: 120_000. */\n timeoutMs?: number;\n /** Max stdout buffer per tool, in bytes. Default: 32 MiB. */\n maxBuffer?: number;\n /**\n * Injectable command runner (for tests). Resolves with the tool's stdout, or\n * rejects with an error carrying `code` (e.g. `\"ENOENT\"`) and, for a non-zero\n * exit, the captured `stdout`. Defaults to a promisified `execFile`.\n */\n exec?: ExecFn;\n /** Injectable directory lister (for tests). Defaults to `fs.readdir`. */\n listDir?: (dir: string) => Promise<string[]>;\n}\n\n/** Shape of an injectable command runner and of the errors it may reject with. */\nexport type ExecFn = (\n command: string,\n args: readonly string[],\n options: { cwd: string; timeout: number; maxBuffer: number },\n) => Promise<{ stdout: string; stderr: string }>;\n\ninterface ExecError {\n code?: string;\n killed?: boolean;\n signal?: string;\n stdout?: string;\n stderr?: string;\n message?: string;\n}\n\nconst DEFAULT_TIMEOUT_MS = 120_000;\nconst DEFAULT_MAX_BUFFER = 32 * 1024 * 1024;\n\n/** One audit tool and how to detect + parse it. */\ninterface AuditTool {\n /** Human label used in diagnostics (e.g. \"cargo audit\"). */\n label: string;\n /** Program to run. */\n command: string;\n /** Arguments (must request JSON output). */\n args: string[];\n /**\n * Given the project's top-level entry names, return the manifest file the\n * advisories should be located at, or null when this ecosystem is absent.\n */\n manifest: (entries: readonly string[]) => string | null;\n /** Parse the tool's JSON stdout into normalized advisory records. */\n parse: (json: unknown) => AdvisoryRecord[];\n}\n\n/** A normalized advisory, ecosystem-independent. */\ninterface AdvisoryRecord {\n /** Advisory id (CVE-…, RUSTSEC-…, GHSA-…, PYSEC-…). */\n id: string;\n package: string;\n version: string;\n summary: string;\n severity: Severity;\n /** Patched version(s), when the tool reports them. */\n patched?: string;\n}\n\n/** Map an ecosystem-reported severity token to our {@link Severity}. */\nfunction toSeverity(raw: unknown): Severity {\n const s = String(raw ?? \"\").toLowerCase();\n if (s === \"critical\") return \"critical\";\n if (s === \"high\") return \"high\";\n if (s === \"moderate\" || s === \"medium\") return \"medium\";\n if (s === \"low\") return \"low\";\n if (s === \"info\" || s === \"informational\" || s === \"none\" || s === \"negligible\") return \"info\";\n // An advisory with no usable severity is treated as high (conservative — a\n // known-vulnerable pinned dependency should not silently pass a scan).\n return \"high\";\n}\n\nfunction asRecord(v: unknown): Record<string, unknown> | null {\n return v !== null && typeof v === \"object\" ? (v as Record<string, unknown>) : null;\n}\nfunction str(v: unknown): string {\n return typeof v === \"string\" ? v : v === undefined || v === null ? \"\" : String(v);\n}\nfunction firstString(v: unknown): string | undefined {\n if (typeof v === \"string\" && v) return v;\n if (Array.isArray(v)) {\n const s = v.find((x) => typeof x === \"string\" && x);\n return typeof s === \"string\" ? s : undefined;\n }\n return undefined;\n}\n\n/** cargo audit --json → advisories. */\nfunction parseCargoAudit(json: unknown): AdvisoryRecord[] {\n const root = asRecord(json);\n const vulns = asRecord(root?.vulnerabilities);\n const list = Array.isArray(vulns?.list) ? vulns.list : [];\n const out: AdvisoryRecord[] = [];\n for (const item of list) {\n const rec = asRecord(item);\n const advisory = asRecord(rec?.advisory);\n const pkg = asRecord(rec?.package);\n const versions = asRecord(rec?.versions);\n if (!advisory) continue;\n out.push({\n id: str(advisory.id) || \"RUSTSEC-UNKNOWN\",\n package: str(pkg?.name) || str(advisory.package),\n version: str(pkg?.version),\n summary: str(advisory.title) || \"security advisory\",\n severity: toSeverity(advisory.severity),\n patched: firstString(versions?.patched),\n });\n }\n return out;\n}\n\n/** pip-audit --format json → advisories. Handles the object + legacy-array forms. */\nfunction parsePipAudit(json: unknown): AdvisoryRecord[] {\n const root = asRecord(json);\n const deps = Array.isArray(json)\n ? json\n : Array.isArray(root?.dependencies)\n ? root.dependencies\n : [];\n const out: AdvisoryRecord[] = [];\n for (const item of deps) {\n const dep = asRecord(item);\n if (!dep) continue;\n const name = str(dep.name);\n const version = str(dep.version);\n const vulns = Array.isArray(dep.vulns) ? dep.vulns : [];\n for (const v of vulns) {\n const vuln = asRecord(v);\n if (!vuln) continue;\n out.push({\n id: str(vuln.id) || firstString(vuln.aliases) || \"PYSEC-UNKNOWN\",\n package: name,\n version,\n summary: str(vuln.description) || \"security advisory\",\n // pip-audit's base JSON does not grade severity; treat as high.\n severity: toSeverity(vuln.severity),\n patched: firstString(vuln.fix_versions),\n });\n }\n }\n return out;\n}\n\n/** Extract a GHSA id from an advisory URL when present. */\nfunction ghsaFrom(url: string): string | undefined {\n const m = /GHSA-[\\w-]+/.exec(url);\n return m ? m[0] : undefined;\n}\n\n/** npm audit --json (npm v7+) → advisories. */\nfunction parseNpmAudit(json: unknown): AdvisoryRecord[] {\n const root = asRecord(json);\n const vulns = asRecord(root?.vulnerabilities);\n if (!vulns) return [];\n const out: AdvisoryRecord[] = [];\n for (const [name, entry] of Object.entries(vulns)) {\n const rec = asRecord(entry);\n if (!rec) continue;\n const range = str(rec.range);\n const fix = asRecord(rec.fixAvailable);\n const patched = fix ? str(fix.version) : undefined;\n const via = Array.isArray(rec.via) ? rec.via : [];\n for (const v of via) {\n const adv = asRecord(v);\n if (!adv) continue; // string `via` = transitive; the source entry carries the detail.\n const url = str(adv.url);\n const id =\n ghsaFrom(url) ||\n (adv.source !== undefined ? `npm-advisory-${str(adv.source)}` : url) ||\n \"npm-advisory\";\n out.push({\n id,\n package: str(adv.name) || name,\n version: range,\n summary: str(adv.title) || \"security advisory\",\n severity: toSeverity(adv.severity ?? rec.severity),\n patched: patched || undefined,\n });\n }\n }\n return out;\n}\n\n/** Match `requirements*.txt` (requirements.txt, requirements-dev.txt, …). */\nfunction isRequirements(name: string): boolean {\n return /^requirements[\\w.-]*\\.txt$/i.test(name);\n}\n\n/** The audit tools, in a deterministic order. */\nconst AUDIT_TOOLS: readonly AuditTool[] = [\n {\n label: \"cargo audit\",\n command: \"cargo\",\n args: [\"audit\", \"--json\"],\n manifest: (e) =>\n e.includes(\"Cargo.lock\") ? \"Cargo.lock\" : e.includes(\"Cargo.toml\") ? \"Cargo.toml\" : null,\n parse: parseCargoAudit,\n },\n {\n label: \"pip-audit\",\n command: \"pip-audit\",\n args: [\"--format\", \"json\"],\n manifest: (e) => {\n const req = e.find(isRequirements);\n if (req) return req;\n return e.includes(\"pyproject.toml\") ? \"pyproject.toml\" : null;\n },\n parse: parsePipAudit,\n },\n {\n label: \"npm audit\",\n command: \"npm\",\n args: [\"audit\", \"--json\"],\n manifest: (e) => (e.includes(\"package-lock.json\") ? \"package-lock.json\" : null),\n parse: parseNpmAudit,\n },\n];\n\n/** Build a {@link Finding} from a normalized advisory record. */\nfunction advisoryFinding(rec: AdvisoryRecord, manifest: string): Finding {\n const pkgVer = rec.version ? `${rec.package}@${rec.version}` : rec.package;\n const finding: Finding = {\n ruleId: \"dep-advisory\",\n title: rec.id,\n category: \"dependency\",\n severity: rec.severity,\n confidence: \"high\",\n hndl: false,\n message: `${pkgVer}: ${rec.summary} (${rec.id})`,\n location: { file: manifest, line: 1 },\n };\n if (rec.patched) finding.remediation = `Upgrade ${rec.package} to ${rec.patched}`;\n return finding;\n}\n\n/**\n * Scan `root` for dependency security advisories by shelling out to each present\n * ecosystem's audit tool. Never throws: a missing tool or a tool error becomes a\n * diagnostic string. Returns the merged findings plus the diagnostics.\n */\nexport async function scanAdvisories(\n root: string,\n opts: ScanAdvisoriesOptions = {},\n): Promise<{ findings: Finding[]; diagnostics: string[] }> {\n const timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const maxBuffer = opts.maxBuffer ?? DEFAULT_MAX_BUFFER;\n const list = opts.listDir ?? ((dir: string) => readdir(dir));\n const exec: ExecFn =\n opts.exec ??\n ((command, args, options) =>\n execFileAsync(command, args as string[], { ...options, windowsHide: true }));\n\n const findings: Finding[] = [];\n const diagnostics: string[] = [];\n\n let entries: string[];\n try {\n entries = await list(root);\n } catch {\n return { findings, diagnostics }; // unreadable root — nothing to audit.\n }\n\n for (const tool of AUDIT_TOOLS) {\n const manifest = tool.manifest(entries);\n if (manifest === null) continue; // this ecosystem isn't present.\n\n let stdout: string;\n try {\n const res = await exec(tool.command, tool.args, { cwd: root, timeout, maxBuffer });\n stdout = res.stdout;\n } catch (err) {\n const e = err as ExecError;\n if (e.code === \"ENOENT\") {\n diagnostics.push(`${tool.label} not available, skipped`);\n continue;\n }\n if (e.killed || e.signal === \"SIGTERM\") {\n diagnostics.push(`${tool.label} timed out, skipped`);\n continue;\n }\n // A non-zero exit is EXPECTED when advisories are found: the JSON is on\n // the error's stdout. Only when there is no parseable stdout is it a real\n // failure we skip over.\n if (typeof e.stdout === \"string\" && e.stdout.trim()) {\n stdout = e.stdout;\n } else {\n const detail = (e.stderr || e.message || \"\").trim().slice(0, 160);\n diagnostics.push(`${tool.label} failed, skipped${detail ? `: ${detail}` : \"\"}`);\n continue;\n }\n }\n\n let json: unknown;\n try {\n json = JSON.parse(stdout);\n } catch {\n diagnostics.push(`${tool.label} produced unparseable output, skipped`);\n continue;\n }\n\n let records: AdvisoryRecord[];\n try {\n records = tool.parse(json);\n } catch {\n diagnostics.push(`${tool.label} output could not be interpreted, skipped`);\n continue;\n }\n\n // Dedupe by advisory id + package (npm lists the same advisory under both a\n // direct and a transitive path).\n const seen = new Set<string>();\n for (const rec of records) {\n const key = `${rec.id}|${rec.package}`;\n if (seen.has(key)) continue;\n seen.add(key);\n findings.push(advisoryFinding(rec, path.posix.basename(manifest)));\n }\n }\n\n return { findings, diagnostics };\n}\n"]} |
| /** | ||
| * Config/any-scope detector: post-quantum KEM parameter / size sanity checks. | ||
| * | ||
| * WHY THIS LIVES IN A PQC-READINESS TOOL. | ||
| * The rest of qScan flags *classical* crypto that must migrate to PQC. This | ||
| * detector is the mirror image: it inspects code that has ALREADY reached for a | ||
| * post-quantum KEM and flags two ways that migration can be quietly wrong — | ||
| * using a *pre-standard* Kyber while claiming FIPS 203, and an internally | ||
| * *inconsistent* parameter set (a byte size that names one ML-KEM/Kyber level | ||
| * while the code advertises a different one). Neither is "quantum-broken", but | ||
| * both defeat the point of migrating: a round-3 Kyber is not interoperable with | ||
| * (nor validated as) FIPS 203 ML-KEM, and a size/level mismatch is a latent bug | ||
| * or a mislabelled security claim. Both rules are `category: "kem"`, | ||
| * `hndl: false` (this is about a PQC primitive's correctness, not a classical | ||
| * confidentiality secret exposed to harvest-now-decrypt-later). | ||
| * | ||
| * RULE 1 — `pqc-prestandard-kem` (medium). | ||
| * Curated identifiers for the well-known PRE-FIPS-203, round-3 CRYSTALS-Kyber | ||
| * packages and APIs (the `pqc_kyber` / `pqcrypto-kyber` / `safe_pqc_kyber` / | ||
| * `crystals-kyber` crates & npm packages, the reference `crypto_kem_kyber768_*` | ||
| * / `pqcrystals_kyber*` API, and the round-3 `Kyber512/768/1024` parameter | ||
| * names). These are NOT ML-KEM: FIPS 203 changed the KDF/domain separation and | ||
| * the algorithm names, so a `Kyber768` build is not a `ML-KEM-768` build. The | ||
| * finding names the exact identifier matched. Confidence starts LOW (using a | ||
| * round-3 Kyber may be a deliberate, documented choice) and is RAISED when the | ||
| * same file also makes a FIPS-203 / ML-KEM / NIST claim — the strong signal that | ||
| * pre-standard Kyber is being passed off as the standard. | ||
| * | ||
| * RULE 2 — `pqc-parameter-mismatch` (medium, LOWER confidence, deliberately | ||
| * conservative). A curated table of the distinctive ML-KEM / Kyber byte sizes: | ||
| * 512 → pk 800, sk 1632 (ct 768 is intentionally omitted: it | ||
| * collides with the level number 768) | ||
| * 768 → pk 1184, sk 2400, ct 1088 | ||
| * 1024 → pk 1568, sk 3168 (pk and ct are both 1568) | ||
| * The rule fires ONLY when a file (already in a Kyber / ML-KEM context) contains | ||
| * one of these exact sizes as a standalone integer AND advertises a DIFFERENT | ||
| * parameter level in text (`ML-KEM-1024`, `Kyber-1024`, …) AND does NOT also | ||
| * advertise the size's own level. So `pk = 1184` (an ML-KEM-768 public key) in a | ||
| * file that calls itself `ML-KEM-1024` fires; a file that mentions both 768 and | ||
| * 1024 (a multi-parameter module) stays silent. Otherwise silent. | ||
| */ | ||
| import type { Detector } from "../types.js"; | ||
| /** | ||
| * Detector: post-quantum KEM parameter / size checks. Fast-rejects any file that | ||
| * does not mention Kyber or ML-KEM at all, so it never touches ordinary code. | ||
| */ | ||
| export declare const pqcParameterDetector: Detector; | ||
| //# sourceMappingURL=pqc-parameter.d.ts.map |
| {"version":3,"file":"pqc-parameter.d.ts","sourceRoot":"","sources":["../../src/detectors/pqc-parameter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAqB,MAAM,aAAa,CAAC;AAsF/D;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,QA6ElC,CAAC"} |
| import { DOC_EXTENSIONS, eachMatch, findingFromRule, hasExtension, maskBlockComments, maskCommentLines, } from "../detect-utils.js"; | ||
| // --- Rule 1: pre-standard round-3 Kyber identifiers ----------------------------- | ||
| // Package / crate names for the round-3 CRYSTALS-Kyber implementations that | ||
| // predate FIPS 203 ML-KEM (Rust crates, npm packages, Python bindings). | ||
| const RE_KYBER_PKG = /\b(?:pqc[_-]?kyber|pqcrypto[_-]kyber|safe_pqc_kyber|crystals[_-]kyber|kyber[_-]crystals|py[_-]?kyber)\b/gi; | ||
| // The reference-implementation C API and its language wrappers. | ||
| const RE_KYBER_API = /\b(?:crypto_kem_kyber(?:512|768|1024)|pqcrystals_kyber(?:512|768|1024))\w*/gi; | ||
| // Round-3 parameter names — distinct from the FIPS 203 `ML-KEM-512/768/1024`. | ||
| const RE_KYBER_PARAM = /\bKyber-?(?:512|768|1024)\b/gi; | ||
| // The generic project name. | ||
| const RE_CRYSTALS_KYBER = /\bCRYSTALS[_-]?Kyber\b/gi; | ||
| const PRESTANDARD_RES = [ | ||
| RE_KYBER_PKG, | ||
| RE_KYBER_API, | ||
| RE_KYBER_PARAM, | ||
| RE_CRYSTALS_KYBER, | ||
| ]; | ||
| /** A FIPS-203 / ML-KEM / NIST claim in the same file — the confidence booster. */ | ||
| const RE_STANDARD_CLAIM = /\bFIPS[\s-]?203\b|\bML-?KEM\b|\bNIST\b/i; | ||
| const RULE_PRESTANDARD = { | ||
| id: "pqc-prestandard-kem", | ||
| title: "Pre-standard (round-3) Kyber, not FIPS 203 ML-KEM", | ||
| description: "A pre-FIPS-203, round-3 CRYSTALS-Kyber package/parameter set used where FIPS 203 ML-KEM is intended", | ||
| category: "kem", | ||
| severity: "medium", | ||
| confidence: "low", | ||
| algorithm: "unknown", | ||
| hndl: false, | ||
| message: "Uses pre-standard round-3 Kyber; this is not FIPS 203 ML-KEM (different KDF/domain separation and not interoperable). Migrate to a FIPS 203 ML-KEM implementation.", | ||
| remediation: "Replace the round-3 Kyber dependency with a FIPS 203 ML-KEM implementation (e.g. ML-KEM-768 / hybrid X25519MLKEM768) and re-run KATs against the FIPS 203 vectors.", | ||
| }; | ||
| // --- Rule 2: ML-KEM / Kyber size ↔ parameter-level mismatch ---------------------- | ||
| /** Distinctive ML-KEM/Kyber byte sizes → the parameter level they belong to. */ | ||
| const SIZE_TO_LEVEL = new Map([ | ||
| [800, 512], | ||
| [1632, 512], | ||
| [1184, 768], | ||
| [2400, 768], | ||
| [1088, 768], | ||
| [1568, 1024], | ||
| [3168, 1024], | ||
| ]); | ||
| // Standalone integer tokens for the distinctive sizes (never inside a longer | ||
| // number or a decimal), so `11840` / `1.088` don't match. | ||
| const RE_SIZE = /(?<![\d.])(800|1632|1184|2400|1088|1568|3168)(?![\d.])/g; | ||
| // An advertised parameter level: `ML-KEM-768`, `MLKEM768`, `Kyber-768`, `Kyber768`. | ||
| const RE_ADVERTISED_LEVEL = /(?:ML-?KEM|Kyber)-?(512|768|1024)\b/gi; | ||
| const RULE_MISMATCH = { | ||
| id: "pqc-parameter-mismatch", | ||
| title: "ML-KEM/Kyber size does not match the advertised parameter set", | ||
| description: "A ML-KEM/Kyber key or ciphertext byte size names one parameter level while the code advertises a different one", | ||
| category: "kem", | ||
| severity: "medium", | ||
| confidence: "low", | ||
| algorithm: "unknown", | ||
| hndl: false, | ||
| message: "A ML-KEM/Kyber byte size does not match the advertised parameter set — likely a mislabelled security level or a copied constant.", | ||
| }; | ||
| /** Collect the distinct advertised parameter levels named in `content`. */ | ||
| function advertisedLevels(content) { | ||
| const levels = new Set(); | ||
| eachMatch(RE_ADVERTISED_LEVEL, content, (m) => levels.add(Number.parseInt(m[1], 10))); | ||
| return levels; | ||
| } | ||
| /** | ||
| * Detector: post-quantum KEM parameter / size checks. Fast-rejects any file that | ||
| * does not mention Kyber or ML-KEM at all, so it never touches ordinary code. | ||
| */ | ||
| export const pqcParameterDetector = { | ||
| id: "pqc-parameter", | ||
| description: "Post-quantum KEM parameter checks: pre-standard round-3 Kyber, and ML-KEM/Kyber size ↔ parameter-set mismatch", | ||
| scope: "config", | ||
| language: "any", | ||
| rules: [RULE_PRESTANDARD, RULE_MISMATCH], | ||
| // Skip prose/docs: a design note discussing Kyber is not live code. | ||
| appliesTo: (f) => !hasExtension(f, DOC_EXTENSIONS), | ||
| detect({ file, content }) { | ||
| // Fast reject: only files that actually reach for a Kyber / ML-KEM KEM. | ||
| if (!/kyber|ml-?kem/i.test(content)) | ||
| return []; | ||
| // Mask comments so a commented-out identifier or a migration note can't fire. | ||
| const scan = maskCommentLines(maskBlockComments(content), ["//", "#", ";"]); | ||
| const findings = []; | ||
| // Rule 1 — pre-standard round-3 Kyber identifiers. The FIPS-203/ML-KEM/NIST | ||
| // *claim* is read from the ORIGINAL content (not the comment-masked copy): a | ||
| // "FIPS 203 compliant" claim in a comment/docstring is exactly the mislabel | ||
| // signal we want to boost confidence on. | ||
| const claimPresent = RE_STANDARD_CLAIM.test(content); | ||
| for (const re of PRESTANDARD_RES) { | ||
| eachMatch(re, scan, (m) => { | ||
| const id = m[0]; | ||
| findings.push(findingFromRule(RULE_PRESTANDARD, { file, content, index: m.index, matchLength: id.length }, { | ||
| // A co-located FIPS-203/ML-KEM/NIST claim is the strong signal that | ||
| // pre-standard Kyber is being passed off as the standard. | ||
| confidence: claimPresent ? "high" : "low", | ||
| message: `Uses pre-standard round-3 Kyber (${id}); not FIPS 203 ML-KEM${claimPresent ? " despite a FIPS-203/ML-KEM/NIST claim in the same file" : ""}. Migrate to a FIPS 203 ML-KEM implementation.`, | ||
| })); | ||
| }); | ||
| } | ||
| // Rule 2 — size ↔ advertised-level mismatch (conservative). The advertised | ||
| // parameter level is read from the ORIGINAL content, since it is very often a | ||
| // comment/docstring label (`// ML-KEM-1024 key sizes`) sitting above the | ||
| // constant. The SIZE token itself is read from the masked copy, so a | ||
| // commented-out constant can't fire. Only meaningful when a level is advertised. | ||
| const advertised = advertisedLevels(content); | ||
| if (advertised.size > 0) { | ||
| // Dedupe by (size,line) so the same constant isn't reported twice. | ||
| const seen = new Set(); | ||
| eachMatch(RE_SIZE, scan, (m) => { | ||
| const size = Number.parseInt(m[0], 10); | ||
| const level = SIZE_TO_LEVEL.get(size); | ||
| if (level === undefined) | ||
| return; | ||
| // Silent when the size's own level is also advertised (a multi-parameter | ||
| // file), or when nothing conflicting is advertised. | ||
| if (advertised.has(level)) | ||
| return; | ||
| const conflict = [...advertised].find((l) => l !== level); | ||
| if (conflict === undefined) | ||
| return; | ||
| const key = `${size}:${m.index}`; | ||
| if (seen.has(key)) | ||
| return; | ||
| seen.add(key); | ||
| findings.push(findingFromRule(RULE_MISMATCH, { file, content, index: m.index, matchLength: m[0].length }, { | ||
| message: `Byte size ${size} matches ML-KEM-${level} but the code advertises ML-KEM-${conflict}; parameter-set mismatch (mislabelled level or a copied constant).`, | ||
| })); | ||
| }); | ||
| } | ||
| return findings; | ||
| }, | ||
| }; | ||
| //# sourceMappingURL=pqc-parameter.js.map |
| {"version":3,"file":"pqc-parameter.js","sourceRoot":"","sources":["../../src/detectors/pqc-parameter.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Config/any-scope detector: post-quantum KEM parameter / size sanity checks.\n *\n * WHY THIS LIVES IN A PQC-READINESS TOOL.\n * The rest of qScan flags *classical* crypto that must migrate to PQC. This\n * detector is the mirror image: it inspects code that has ALREADY reached for a\n * post-quantum KEM and flags two ways that migration can be quietly wrong —\n * using a *pre-standard* Kyber while claiming FIPS 203, and an internally\n * *inconsistent* parameter set (a byte size that names one ML-KEM/Kyber level\n * while the code advertises a different one). Neither is \"quantum-broken\", but\n * both defeat the point of migrating: a round-3 Kyber is not interoperable with\n * (nor validated as) FIPS 203 ML-KEM, and a size/level mismatch is a latent bug\n * or a mislabelled security claim. Both rules are `category: \"kem\"`,\n * `hndl: false` (this is about a PQC primitive's correctness, not a classical\n * confidentiality secret exposed to harvest-now-decrypt-later).\n *\n * RULE 1 — `pqc-prestandard-kem` (medium).\n * Curated identifiers for the well-known PRE-FIPS-203, round-3 CRYSTALS-Kyber\n * packages and APIs (the `pqc_kyber` / `pqcrypto-kyber` / `safe_pqc_kyber` /\n * `crystals-kyber` crates & npm packages, the reference `crypto_kem_kyber768_*`\n * / `pqcrystals_kyber*` API, and the round-3 `Kyber512/768/1024` parameter\n * names). These are NOT ML-KEM: FIPS 203 changed the KDF/domain separation and\n * the algorithm names, so a `Kyber768` build is not a `ML-KEM-768` build. The\n * finding names the exact identifier matched. Confidence starts LOW (using a\n * round-3 Kyber may be a deliberate, documented choice) and is RAISED when the\n * same file also makes a FIPS-203 / ML-KEM / NIST claim — the strong signal that\n * pre-standard Kyber is being passed off as the standard.\n *\n * RULE 2 — `pqc-parameter-mismatch` (medium, LOWER confidence, deliberately\n * conservative). A curated table of the distinctive ML-KEM / Kyber byte sizes:\n * 512 → pk 800, sk 1632 (ct 768 is intentionally omitted: it\n * collides with the level number 768)\n * 768 → pk 1184, sk 2400, ct 1088\n * 1024 → pk 1568, sk 3168 (pk and ct are both 1568)\n * The rule fires ONLY when a file (already in a Kyber / ML-KEM context) contains\n * one of these exact sizes as a standalone integer AND advertises a DIFFERENT\n * parameter level in text (`ML-KEM-1024`, `Kyber-1024`, …) AND does NOT also\n * advertise the size's own level. So `pk = 1184` (an ML-KEM-768 public key) in a\n * file that calls itself `ML-KEM-1024` fires; a file that mentions both 768 and\n * 1024 (a multi-parameter module) stays silent. Otherwise silent.\n */\nimport type { Detector, Finding, RuleMeta } from \"../types.js\";\nimport {\n DOC_EXTENSIONS,\n eachMatch,\n findingFromRule,\n hasExtension,\n maskBlockComments,\n maskCommentLines,\n} from \"../detect-utils.js\";\n\n// --- Rule 1: pre-standard round-3 Kyber identifiers -----------------------------\n// Package / crate names for the round-3 CRYSTALS-Kyber implementations that\n// predate FIPS 203 ML-KEM (Rust crates, npm packages, Python bindings).\nconst RE_KYBER_PKG =\n /\\b(?:pqc[_-]?kyber|pqcrypto[_-]kyber|safe_pqc_kyber|crystals[_-]kyber|kyber[_-]crystals|py[_-]?kyber)\\b/gi;\n// The reference-implementation C API and its language wrappers.\nconst RE_KYBER_API = /\\b(?:crypto_kem_kyber(?:512|768|1024)|pqcrystals_kyber(?:512|768|1024))\\w*/gi;\n// Round-3 parameter names — distinct from the FIPS 203 `ML-KEM-512/768/1024`.\nconst RE_KYBER_PARAM = /\\bKyber-?(?:512|768|1024)\\b/gi;\n// The generic project name.\nconst RE_CRYSTALS_KYBER = /\\bCRYSTALS[_-]?Kyber\\b/gi;\n\nconst PRESTANDARD_RES: readonly RegExp[] = [\n RE_KYBER_PKG,\n RE_KYBER_API,\n RE_KYBER_PARAM,\n RE_CRYSTALS_KYBER,\n];\n\n/** A FIPS-203 / ML-KEM / NIST claim in the same file — the confidence booster. */\nconst RE_STANDARD_CLAIM = /\\bFIPS[\\s-]?203\\b|\\bML-?KEM\\b|\\bNIST\\b/i;\n\nconst RULE_PRESTANDARD: RuleMeta = {\n id: \"pqc-prestandard-kem\",\n title: \"Pre-standard (round-3) Kyber, not FIPS 203 ML-KEM\",\n description:\n \"A pre-FIPS-203, round-3 CRYSTALS-Kyber package/parameter set used where FIPS 203 ML-KEM is intended\",\n category: \"kem\",\n severity: \"medium\",\n confidence: \"low\",\n algorithm: \"unknown\",\n hndl: false,\n message:\n \"Uses pre-standard round-3 Kyber; this is not FIPS 203 ML-KEM (different KDF/domain separation and not interoperable). Migrate to a FIPS 203 ML-KEM implementation.\",\n remediation:\n \"Replace the round-3 Kyber dependency with a FIPS 203 ML-KEM implementation (e.g. ML-KEM-768 / hybrid X25519MLKEM768) and re-run KATs against the FIPS 203 vectors.\",\n};\n\n// --- Rule 2: ML-KEM / Kyber size ↔ parameter-level mismatch ----------------------\n/** Distinctive ML-KEM/Kyber byte sizes → the parameter level they belong to. */\nconst SIZE_TO_LEVEL: ReadonlyMap<number, 512 | 768 | 1024> = new Map([\n [800, 512],\n [1632, 512],\n [1184, 768],\n [2400, 768],\n [1088, 768],\n [1568, 1024],\n [3168, 1024],\n]);\n// Standalone integer tokens for the distinctive sizes (never inside a longer\n// number or a decimal), so `11840` / `1.088` don't match.\nconst RE_SIZE = /(?<![\\d.])(800|1632|1184|2400|1088|1568|3168)(?![\\d.])/g;\n// An advertised parameter level: `ML-KEM-768`, `MLKEM768`, `Kyber-768`, `Kyber768`.\nconst RE_ADVERTISED_LEVEL = /(?:ML-?KEM|Kyber)-?(512|768|1024)\\b/gi;\n\nconst RULE_MISMATCH: RuleMeta = {\n id: \"pqc-parameter-mismatch\",\n title: \"ML-KEM/Kyber size does not match the advertised parameter set\",\n description:\n \"A ML-KEM/Kyber key or ciphertext byte size names one parameter level while the code advertises a different one\",\n category: \"kem\",\n severity: \"medium\",\n confidence: \"low\",\n algorithm: \"unknown\",\n hndl: false,\n message:\n \"A ML-KEM/Kyber byte size does not match the advertised parameter set — likely a mislabelled security level or a copied constant.\",\n};\n\n/** Collect the distinct advertised parameter levels named in `content`. */\nfunction advertisedLevels(content: string): Set<number> {\n const levels = new Set<number>();\n eachMatch(RE_ADVERTISED_LEVEL, content, (m) => levels.add(Number.parseInt(m[1], 10)));\n return levels;\n}\n\n/**\n * Detector: post-quantum KEM parameter / size checks. Fast-rejects any file that\n * does not mention Kyber or ML-KEM at all, so it never touches ordinary code.\n */\nexport const pqcParameterDetector: Detector = {\n id: \"pqc-parameter\",\n description:\n \"Post-quantum KEM parameter checks: pre-standard round-3 Kyber, and ML-KEM/Kyber size ↔ parameter-set mismatch\",\n scope: \"config\",\n language: \"any\",\n rules: [RULE_PRESTANDARD, RULE_MISMATCH],\n // Skip prose/docs: a design note discussing Kyber is not live code.\n appliesTo: (f) => !hasExtension(f, DOC_EXTENSIONS),\n detect({ file, content }): Finding[] {\n // Fast reject: only files that actually reach for a Kyber / ML-KEM KEM.\n if (!/kyber|ml-?kem/i.test(content)) return [];\n\n // Mask comments so a commented-out identifier or a migration note can't fire.\n const scan = maskCommentLines(maskBlockComments(content), [\"//\", \"#\", \";\"]);\n const findings: Finding[] = [];\n\n // Rule 1 — pre-standard round-3 Kyber identifiers. The FIPS-203/ML-KEM/NIST\n // *claim* is read from the ORIGINAL content (not the comment-masked copy): a\n // \"FIPS 203 compliant\" claim in a comment/docstring is exactly the mislabel\n // signal we want to boost confidence on.\n const claimPresent = RE_STANDARD_CLAIM.test(content);\n for (const re of PRESTANDARD_RES) {\n eachMatch(re, scan, (m) => {\n const id = m[0];\n findings.push(\n findingFromRule(\n RULE_PRESTANDARD,\n { file, content, index: m.index, matchLength: id.length },\n {\n // A co-located FIPS-203/ML-KEM/NIST claim is the strong signal that\n // pre-standard Kyber is being passed off as the standard.\n confidence: claimPresent ? \"high\" : \"low\",\n message: `Uses pre-standard round-3 Kyber (${id}); not FIPS 203 ML-KEM${\n claimPresent ? \" despite a FIPS-203/ML-KEM/NIST claim in the same file\" : \"\"\n }. Migrate to a FIPS 203 ML-KEM implementation.`,\n },\n ),\n );\n });\n }\n\n // Rule 2 — size ↔ advertised-level mismatch (conservative). The advertised\n // parameter level is read from the ORIGINAL content, since it is very often a\n // comment/docstring label (`// ML-KEM-1024 key sizes`) sitting above the\n // constant. The SIZE token itself is read from the masked copy, so a\n // commented-out constant can't fire. Only meaningful when a level is advertised.\n const advertised = advertisedLevels(content);\n if (advertised.size > 0) {\n // Dedupe by (size,line) so the same constant isn't reported twice.\n const seen = new Set<string>();\n eachMatch(RE_SIZE, scan, (m) => {\n const size = Number.parseInt(m[0], 10);\n const level = SIZE_TO_LEVEL.get(size);\n if (level === undefined) return;\n // Silent when the size's own level is also advertised (a multi-parameter\n // file), or when nothing conflicting is advertised.\n if (advertised.has(level)) return;\n const conflict = [...advertised].find((l) => l !== level);\n if (conflict === undefined) return;\n const key = `${size}:${m.index}`;\n if (seen.has(key)) return;\n seen.add(key);\n findings.push(\n findingFromRule(\n RULE_MISMATCH,\n { file, content, index: m.index, matchLength: m[0].length },\n {\n message: `Byte size ${size} matches ML-KEM-${level} but the code advertises ML-KEM-${conflict}; parameter-set mismatch (mislabelled level or a copied constant).`,\n },\n ),\n );\n });\n }\n\n return findings;\n },\n};\n"]} |
| import type { Finding } from "./types.js"; | ||
| /** Outcome of a HEAD request to a declared repository URL. */ | ||
| export type RepoHeadOutcome = { | ||
| kind: "status"; | ||
| status: number; | ||
| } | { | ||
| kind: "unresolved"; | ||
| } | { | ||
| kind: "error"; | ||
| message: string; | ||
| }; | ||
| /** | ||
| * Injected HEAD requester. Implemented by the (networked) caller — qScan supplies | ||
| * a `node:https`-backed one — so core never imports an outbound network module. | ||
| * MUST resolve (never reject); map its own failures onto {@link RepoHeadOutcome}. | ||
| */ | ||
| export type RepoHeadRequester = (url: string, timeoutMs: number) => Promise<RepoHeadOutcome>; | ||
| /** Options for {@link checkProvenance}. */ | ||
| export interface ProvenanceOptions { | ||
| /** | ||
| * Verify the declared repository over the network via {@link head}. When false | ||
| * (or when no `head` is supplied) only the static `repo-missing` check runs. | ||
| */ | ||
| network?: boolean; | ||
| /** HEAD-request timeout in milliseconds. Default: 5000. */ | ||
| timeoutMs?: number; | ||
| /** Injected HEAD requester (required for the network check to do anything). */ | ||
| head?: RepoHeadRequester; | ||
| /** Injected file reader (for tests). Defaults to `fs.readFile`. */ | ||
| readManifest?: (file: string) => Promise<string>; | ||
| } | ||
| /** | ||
| * Normalize a declared repository reference to an https(s) URL, or null when it | ||
| * cannot be turned into one we can HEAD. Handles `git+https://…`, `git://…`, | ||
| * `git@github.com:owner/repo(.git)`, the `owner/repo` and `github:owner/repo` | ||
| * shorthands, and strips a trailing `.git`. | ||
| */ | ||
| export declare function normalizeRepoUrl(raw: string): string | null; | ||
| /** | ||
| * Check a project's declared-source-repository provenance. | ||
| * | ||
| * Static (always): a present root manifest that declares NO repository yields a | ||
| * `provenance-repo-missing` info finding. Network (`network: true` + a `head` | ||
| * requester): a declared repository that 404s or does not resolve yields a | ||
| * `provenance-repo-unresolved` (medium) finding; transient network errors are | ||
| * skipped silently (recorded as a diagnostic). Never throws. | ||
| */ | ||
| export declare function checkProvenance(root: string, opts?: ProvenanceOptions): Promise<{ | ||
| findings: Finding[]; | ||
| diagnostics: string[]; | ||
| }>; | ||
| /** The two rule ids this module can emit, for SARIF catalog registration. */ | ||
| export declare const PROVENANCE_RULES: import("./types.js").RuleMeta[]; | ||
| //# sourceMappingURL=provenance.d.ts.map |
| {"version":3,"file":"provenance.d.ts","sourceRoot":"","sources":["../src/provenance.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,8DAA8D;AAC9D,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,GACtB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvC;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;AAE7F,2CAA2C;AAC3C,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+EAA+E;IAC/E,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB,mEAAmE;IACnE,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAClD;AAeD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAmB3D;AA8DD;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,iBAAsB,GAC3B,OAAO,CAAC;IAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;IAAC,WAAW,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CA2DzD;AAED,6EAA6E;AAC7E,eAAO,MAAM,gBAAgB,EAAE,OAAO,YAAY,EAAE,QAAQ,EAsB3D,CAAC"} |
| /** | ||
| * Provenance / declared-source-repository check (wired to `qscan --audit`). | ||
| * | ||
| * A package that declares no source repository, or one whose declared repository | ||
| * does not resolve, cannot have its published artifacts verified against source — | ||
| * a supply-chain gap. This reads the ROOT manifest's repository URL and emits: | ||
| * | ||
| * - `provenance-repo-missing` (info) — the manifest declares no repository. | ||
| * - `provenance-repo-unresolved`(medium) — the declared repository 404s / does | ||
| * not resolve (network mode only). | ||
| * | ||
| * OFFLINE BOUNDARY (ADR-0005). `@quantakrypto/core` must stay strictly offline: it | ||
| * may NOT import `node:https` or make outbound calls. So this module does the | ||
| * pure work — parse the manifest, decide what to check — and delegates the actual | ||
| * HEAD request to an INJECTED {@link RepoHeadRequester} that the caller (qScan, | ||
| * which is allowed to be networked) supplies. In the default (static) mode no | ||
| * network hook is used at all, so a scan without `--audit` never reaches for one. | ||
| */ | ||
| import { readFile } from "node:fs/promises"; | ||
| import * as path from "node:path"; | ||
| const DEFAULT_TIMEOUT_MS = 5000; | ||
| /** The root manifests we understand, in precedence order. */ | ||
| const MANIFESTS = ["package.json", "Cargo.toml", "pyproject.toml"]; | ||
| /** | ||
| * Normalize a declared repository reference to an https(s) URL, or null when it | ||
| * cannot be turned into one we can HEAD. Handles `git+https://…`, `git://…`, | ||
| * `git@github.com:owner/repo(.git)`, the `owner/repo` and `github:owner/repo` | ||
| * shorthands, and strips a trailing `.git`. | ||
| */ | ||
| export function normalizeRepoUrl(raw) { | ||
| let s = raw.trim(); | ||
| if (!s) | ||
| return null; | ||
| s = s.replace(/^git\+/, ""); | ||
| // scp-style `git@host:owner/repo` | ||
| const scp = /^[\w.-]+@([\w.-]+):(.+)$/.exec(s); | ||
| if (scp) | ||
| s = `https://${scp[1]}/${scp[2]}`; | ||
| if (s.startsWith("git://")) | ||
| s = `https://${s.slice("git://".length)}`; | ||
| if (s.startsWith("ssh://")) | ||
| s = `https://${s.slice("ssh://".length)}`; | ||
| // `github:owner/repo` / `gitlab:owner/repo` / `bitbucket:owner/repo` | ||
| const hosted = /^(github|gitlab|bitbucket):(.+)$/.exec(s); | ||
| if (hosted) { | ||
| const host = hosted[1] === "github" ? "github.com" : `${hosted[1]}.org`; | ||
| s = `https://${host}/${hosted[2]}`; | ||
| } | ||
| // bare `owner/repo` shorthand → GitHub | ||
| if (/^[\w.-]+\/[\w.-]+$/.test(s)) | ||
| s = `https://github.com/${s}`; | ||
| if (!/^https?:\/\//i.test(s)) | ||
| return null; | ||
| return s.replace(/\.git$/, ""); | ||
| } | ||
| /** package.json `repository` (string or `{ url }`). */ | ||
| function repoFromPackageJson(content) { | ||
| let json; | ||
| try { | ||
| json = JSON.parse(content); | ||
| } | ||
| catch { | ||
| return null; | ||
| } | ||
| if (json === null || typeof json !== "object") | ||
| return null; | ||
| const repo = json.repository; | ||
| if (typeof repo === "string") | ||
| return normalizeRepoUrl(repo); | ||
| if (repo !== null && typeof repo === "object") { | ||
| const url = repo.url; | ||
| if (typeof url === "string") | ||
| return normalizeRepoUrl(url); | ||
| } | ||
| return null; | ||
| } | ||
| /** Cargo.toml `[package] repository = "…"`. */ | ||
| function repoFromCargoToml(content) { | ||
| const m = /^\s*repository\s*=\s*"([^"]+)"/m.exec(content); | ||
| return m ? normalizeRepoUrl(m[1]) : null; | ||
| } | ||
| /** | ||
| * pyproject.toml — `[project.urls]` `Repository`/`Source`/`Homepage`, or the | ||
| * legacy `[tool.poetry]` `repository = "…"`. Generous line scan (any of those | ||
| * keys → url), which is enough to know a repository was declared. | ||
| */ | ||
| function repoFromPyproject(content) { | ||
| const m = /^\s*(?:repository|source|homepage)\s*=\s*"([^"]+)"/im.exec(content); | ||
| return m ? normalizeRepoUrl(m[1]) : null; | ||
| } | ||
| const PARSERS = { | ||
| "package.json": repoFromPackageJson, | ||
| "Cargo.toml": repoFromCargoToml, | ||
| "pyproject.toml": repoFromPyproject, | ||
| }; | ||
| /** | ||
| * Read the first present root manifest and extract its declared repository URL. | ||
| * Returns null when NO root manifest exists (there is no package to assess). | ||
| */ | ||
| async function readManifestRepo(root, read) { | ||
| for (const file of MANIFESTS) { | ||
| let content; | ||
| try { | ||
| content = await read(path.join(root, file)); | ||
| } | ||
| catch { | ||
| continue; // manifest absent / unreadable — try the next. | ||
| } | ||
| return { file, url: PARSERS[file](content) }; | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Check a project's declared-source-repository provenance. | ||
| * | ||
| * Static (always): a present root manifest that declares NO repository yields a | ||
| * `provenance-repo-missing` info finding. Network (`network: true` + a `head` | ||
| * requester): a declared repository that 404s or does not resolve yields a | ||
| * `provenance-repo-unresolved` (medium) finding; transient network errors are | ||
| * skipped silently (recorded as a diagnostic). Never throws. | ||
| */ | ||
| export async function checkProvenance(root, opts = {}) { | ||
| const read = opts.readManifest ?? ((file) => readFile(file, "utf8")); | ||
| const findings = []; | ||
| const diagnostics = []; | ||
| const manifest = await readManifestRepo(root, read); | ||
| if (manifest === null) | ||
| return { findings, diagnostics }; // no manifest → nothing to assess. | ||
| if (manifest.url === null) { | ||
| findings.push({ | ||
| ruleId: "provenance-repo-missing", | ||
| title: "Package declares no source repository", | ||
| category: "dependency", | ||
| severity: "info", | ||
| confidence: "medium", | ||
| hndl: false, | ||
| message: "Package declares no source repository; builds cannot be verified against source.", | ||
| remediation: "Declare a source repository in the manifest (package.json `repository`, Cargo.toml `repository`, or pyproject.toml `[project.urls]`).", | ||
| location: { file: path.posix.basename(manifest.file), line: 1 }, | ||
| }); | ||
| return { findings, diagnostics }; | ||
| } | ||
| // Network verification is opt-in and needs an injected requester. | ||
| if (!opts.network || !opts.head) | ||
| return { findings, diagnostics }; | ||
| const timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS; | ||
| let outcome; | ||
| try { | ||
| outcome = await opts.head(manifest.url, timeout); | ||
| } | ||
| catch (err) { | ||
| const message = err instanceof Error ? err.message : String(err); | ||
| diagnostics.push(`provenance: could not verify ${manifest.url} (${message}), skipped`); | ||
| return { findings, diagnostics }; | ||
| } | ||
| const unresolved = outcome.kind === "unresolved" || | ||
| (outcome.kind === "status" && (outcome.status === 404 || outcome.status === 410)); | ||
| if (unresolved) { | ||
| findings.push({ | ||
| ruleId: "provenance-repo-unresolved", | ||
| title: "Declared source repository does not resolve", | ||
| category: "dependency", | ||
| severity: "medium", | ||
| confidence: "medium", | ||
| hndl: false, | ||
| message: `Declared source repository ${manifest.url} does not resolve.`, | ||
| remediation: "Fix the manifest's repository URL to point at the real, reachable source repository.", | ||
| location: { file: path.posix.basename(manifest.file), line: 1 }, | ||
| }); | ||
| } | ||
| else if (outcome.kind === "error") { | ||
| diagnostics.push(`provenance: could not verify ${manifest.url} (${outcome.message}), skipped`); | ||
| } | ||
| return { findings, diagnostics }; | ||
| } | ||
| /** The two rule ids this module can emit, for SARIF catalog registration. */ | ||
| export const PROVENANCE_RULES = [ | ||
| { | ||
| id: "provenance-repo-missing", | ||
| title: "Package declares no source repository", | ||
| category: "dependency", | ||
| severity: "info", | ||
| confidence: "medium", | ||
| hndl: false, | ||
| message: "Package declares no source repository; builds cannot be verified against source.", | ||
| description: "The root manifest declares no source repository (opt-in with `qscan --audit`).", | ||
| }, | ||
| { | ||
| id: "provenance-repo-unresolved", | ||
| title: "Declared source repository does not resolve", | ||
| category: "dependency", | ||
| severity: "medium", | ||
| confidence: "medium", | ||
| hndl: false, | ||
| message: "The declared source repository does not resolve (404 / DNS failure).", | ||
| description: "The manifest's declared source repository 404s or does not resolve (opt-in with `qscan --audit`).", | ||
| }, | ||
| ]; | ||
| //# sourceMappingURL=provenance.js.map |
| {"version":3,"file":"provenance.js","sourceRoot":"","sources":["../src/provenance.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Provenance / declared-source-repository check (wired to `qscan --audit`).\n *\n * A package that declares no source repository, or one whose declared repository\n * does not resolve, cannot have its published artifacts verified against source —\n * a supply-chain gap. This reads the ROOT manifest's repository URL and emits:\n *\n * - `provenance-repo-missing` (info) — the manifest declares no repository.\n * - `provenance-repo-unresolved`(medium) — the declared repository 404s / does\n * not resolve (network mode only).\n *\n * OFFLINE BOUNDARY (ADR-0005). `@quantakrypto/core` must stay strictly offline: it\n * may NOT import `node:https` or make outbound calls. So this module does the\n * pure work — parse the manifest, decide what to check — and delegates the actual\n * HEAD request to an INJECTED {@link RepoHeadRequester} that the caller (qScan,\n * which is allowed to be networked) supplies. In the default (static) mode no\n * network hook is used at all, so a scan without `--audit` never reaches for one.\n */\nimport { readFile } from \"node:fs/promises\";\nimport * as path from \"node:path\";\n\nimport type { Finding } from \"./types.js\";\n\n/** Outcome of a HEAD request to a declared repository URL. */\nexport type RepoHeadOutcome =\n | { kind: \"status\"; status: number } // an HTTP response was received.\n | { kind: \"unresolved\" } // DNS / host-not-found — the repository does not exist.\n | { kind: \"error\"; message: string }; // transient network error — verification skipped.\n\n/**\n * Injected HEAD requester. Implemented by the (networked) caller — qScan supplies\n * a `node:https`-backed one — so core never imports an outbound network module.\n * MUST resolve (never reject); map its own failures onto {@link RepoHeadOutcome}.\n */\nexport type RepoHeadRequester = (url: string, timeoutMs: number) => Promise<RepoHeadOutcome>;\n\n/** Options for {@link checkProvenance}. */\nexport interface ProvenanceOptions {\n /**\n * Verify the declared repository over the network via {@link head}. When false\n * (or when no `head` is supplied) only the static `repo-missing` check runs.\n */\n network?: boolean;\n /** HEAD-request timeout in milliseconds. Default: 5000. */\n timeoutMs?: number;\n /** Injected HEAD requester (required for the network check to do anything). */\n head?: RepoHeadRequester;\n /** Injected file reader (for tests). Defaults to `fs.readFile`. */\n readManifest?: (file: string) => Promise<string>;\n}\n\nconst DEFAULT_TIMEOUT_MS = 5000;\n\n/** The root manifests we understand, in precedence order. */\nconst MANIFESTS = [\"package.json\", \"Cargo.toml\", \"pyproject.toml\"] as const;\n\n/** What a root manifest told us about its declared repository. */\ninterface ManifestRepo {\n /** The manifest filename that was read. */\n file: string;\n /** The declared repository URL, or null when the manifest declares none. */\n url: string | null;\n}\n\n/**\n * Normalize a declared repository reference to an https(s) URL, or null when it\n * cannot be turned into one we can HEAD. Handles `git+https://…`, `git://…`,\n * `git@github.com:owner/repo(.git)`, the `owner/repo` and `github:owner/repo`\n * shorthands, and strips a trailing `.git`.\n */\nexport function normalizeRepoUrl(raw: string): string | null {\n let s = raw.trim();\n if (!s) return null;\n s = s.replace(/^git\\+/, \"\");\n // scp-style `git@host:owner/repo`\n const scp = /^[\\w.-]+@([\\w.-]+):(.+)$/.exec(s);\n if (scp) s = `https://${scp[1]}/${scp[2]}`;\n if (s.startsWith(\"git://\")) s = `https://${s.slice(\"git://\".length)}`;\n if (s.startsWith(\"ssh://\")) s = `https://${s.slice(\"ssh://\".length)}`;\n // `github:owner/repo` / `gitlab:owner/repo` / `bitbucket:owner/repo`\n const hosted = /^(github|gitlab|bitbucket):(.+)$/.exec(s);\n if (hosted) {\n const host = hosted[1] === \"github\" ? \"github.com\" : `${hosted[1]}.org`;\n s = `https://${host}/${hosted[2]}`;\n }\n // bare `owner/repo` shorthand → GitHub\n if (/^[\\w.-]+\\/[\\w.-]+$/.test(s)) s = `https://github.com/${s}`;\n if (!/^https?:\\/\\//i.test(s)) return null;\n return s.replace(/\\.git$/, \"\");\n}\n\n/** package.json `repository` (string or `{ url }`). */\nfunction repoFromPackageJson(content: string): string | null {\n let json: unknown;\n try {\n json = JSON.parse(content);\n } catch {\n return null;\n }\n if (json === null || typeof json !== \"object\") return null;\n const repo = (json as Record<string, unknown>).repository;\n if (typeof repo === \"string\") return normalizeRepoUrl(repo);\n if (repo !== null && typeof repo === \"object\") {\n const url = (repo as Record<string, unknown>).url;\n if (typeof url === \"string\") return normalizeRepoUrl(url);\n }\n return null;\n}\n\n/** Cargo.toml `[package] repository = \"…\"`. */\nfunction repoFromCargoToml(content: string): string | null {\n const m = /^\\s*repository\\s*=\\s*\"([^\"]+)\"/m.exec(content);\n return m ? normalizeRepoUrl(m[1]) : null;\n}\n\n/**\n * pyproject.toml — `[project.urls]` `Repository`/`Source`/`Homepage`, or the\n * legacy `[tool.poetry]` `repository = \"…\"`. Generous line scan (any of those\n * keys → url), which is enough to know a repository was declared.\n */\nfunction repoFromPyproject(content: string): string | null {\n const m = /^\\s*(?:repository|source|homepage)\\s*=\\s*\"([^\"]+)\"/im.exec(content);\n return m ? normalizeRepoUrl(m[1]) : null;\n}\n\nconst PARSERS: Record<(typeof MANIFESTS)[number], (content: string) => string | null> = {\n \"package.json\": repoFromPackageJson,\n \"Cargo.toml\": repoFromCargoToml,\n \"pyproject.toml\": repoFromPyproject,\n};\n\n/**\n * Read the first present root manifest and extract its declared repository URL.\n * Returns null when NO root manifest exists (there is no package to assess).\n */\nasync function readManifestRepo(\n root: string,\n read: (file: string) => Promise<string>,\n): Promise<ManifestRepo | null> {\n for (const file of MANIFESTS) {\n let content: string;\n try {\n content = await read(path.join(root, file));\n } catch {\n continue; // manifest absent / unreadable — try the next.\n }\n return { file, url: PARSERS[file](content) };\n }\n return null;\n}\n\n/**\n * Check a project's declared-source-repository provenance.\n *\n * Static (always): a present root manifest that declares NO repository yields a\n * `provenance-repo-missing` info finding. Network (`network: true` + a `head`\n * requester): a declared repository that 404s or does not resolve yields a\n * `provenance-repo-unresolved` (medium) finding; transient network errors are\n * skipped silently (recorded as a diagnostic). Never throws.\n */\nexport async function checkProvenance(\n root: string,\n opts: ProvenanceOptions = {},\n): Promise<{ findings: Finding[]; diagnostics: string[] }> {\n const read = opts.readManifest ?? ((file: string) => readFile(file, \"utf8\"));\n const findings: Finding[] = [];\n const diagnostics: string[] = [];\n\n const manifest = await readManifestRepo(root, read);\n if (manifest === null) return { findings, diagnostics }; // no manifest → nothing to assess.\n\n if (manifest.url === null) {\n findings.push({\n ruleId: \"provenance-repo-missing\",\n title: \"Package declares no source repository\",\n category: \"dependency\",\n severity: \"info\",\n confidence: \"medium\",\n hndl: false,\n message: \"Package declares no source repository; builds cannot be verified against source.\",\n remediation:\n \"Declare a source repository in the manifest (package.json `repository`, Cargo.toml `repository`, or pyproject.toml `[project.urls]`).\",\n location: { file: path.posix.basename(manifest.file), line: 1 },\n });\n return { findings, diagnostics };\n }\n\n // Network verification is opt-in and needs an injected requester.\n if (!opts.network || !opts.head) return { findings, diagnostics };\n\n const timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n let outcome: RepoHeadOutcome;\n try {\n outcome = await opts.head(manifest.url, timeout);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n diagnostics.push(`provenance: could not verify ${manifest.url} (${message}), skipped`);\n return { findings, diagnostics };\n }\n\n const unresolved =\n outcome.kind === \"unresolved\" ||\n (outcome.kind === \"status\" && (outcome.status === 404 || outcome.status === 410));\n\n if (unresolved) {\n findings.push({\n ruleId: \"provenance-repo-unresolved\",\n title: \"Declared source repository does not resolve\",\n category: \"dependency\",\n severity: \"medium\",\n confidence: \"medium\",\n hndl: false,\n message: `Declared source repository ${manifest.url} does not resolve.`,\n remediation:\n \"Fix the manifest's repository URL to point at the real, reachable source repository.\",\n location: { file: path.posix.basename(manifest.file), line: 1 },\n });\n } else if (outcome.kind === \"error\") {\n diagnostics.push(`provenance: could not verify ${manifest.url} (${outcome.message}), skipped`);\n }\n\n return { findings, diagnostics };\n}\n\n/** The two rule ids this module can emit, for SARIF catalog registration. */\nexport const PROVENANCE_RULES: import(\"./types.js\").RuleMeta[] = [\n {\n id: \"provenance-repo-missing\",\n title: \"Package declares no source repository\",\n category: \"dependency\",\n severity: \"info\",\n confidence: \"medium\",\n hndl: false,\n message: \"Package declares no source repository; builds cannot be verified against source.\",\n description: \"The root manifest declares no source repository (opt-in with `qscan --audit`).\",\n },\n {\n id: \"provenance-repo-unresolved\",\n title: \"Declared source repository does not resolve\",\n category: \"dependency\",\n severity: \"medium\",\n confidence: \"medium\",\n hndl: false,\n message: \"The declared source repository does not resolve (404 / DNS failure).\",\n description:\n \"The manifest's declared source repository 404s or does not resolve (opt-in with `qscan --audit`).\",\n },\n];\n"]} |
| /** | ||
| * Dependency-advisory scanning (opt-in, wired to `qscan --audit`). | ||
| * | ||
| * Unlike the built-in {@link vulnerableDependencies} database — which flags | ||
| * packages whose *purpose* is quantum-vulnerable classical crypto — this module | ||
| * surfaces KNOWN SECURITY ADVISORIES (CVE / RUSTSEC / GHSA / PYSEC) against the | ||
| * pinned versions in a project's lockfiles, by shelling out to each ecosystem's | ||
| * own audit tool: | ||
| * - Rust (`Cargo.toml` / `Cargo.lock`) → `cargo audit --json` | ||
| * - Python(`requirements*.txt` / `pyproject.toml`)→ `pip-audit --format json` | ||
| * - npm (`package-lock.json`) → `npm audit --json` | ||
| * | ||
| * DESIGN (mirrors `changed.ts` / `sign.ts`, the blessed shell-out pattern): | ||
| * - `execFile` (never a shell), a timeout, and a bounded `maxBuffer`, all inside | ||
| * a try/catch. A missing tool (`ENOENT`) or any other failure NEVER throws — | ||
| * it degrades to a diagnostic string ("cargo audit not available, skipped"). | ||
| * - The audit tools exit NON-ZERO when they find advisories, so their JSON | ||
| * arrives on the error's `stdout`; that is the normal, expected path. | ||
| * - Zero runtime dependencies (ADR-0001): only `node:child_process` / | ||
| * `node:util` / `node:fs` — the same built-ins `changed.ts` already uses. | ||
| * | ||
| * Findings are `category: "dependency"`, `ruleId: "dep-advisory"`, located at the | ||
| * project's manifest file. They are produced by this scanner, not a registered | ||
| * {@link Detector}, so — exactly like `DEP_VULNERABLE_RULE` — the generic | ||
| * {@link DEP_ADVISORY_RULE} catalog entry is merged into the SARIF `rules[]` by | ||
| * the reporter (see qscan `report.ts`). | ||
| */ | ||
| import { execFile } from "node:child_process"; | ||
| import { promisify } from "node:util"; | ||
| import { readdir } from "node:fs/promises"; | ||
| import * as path from "node:path"; | ||
| import type { Finding, RuleMeta, Severity } from "./types.js"; | ||
| const execFileAsync = promisify(execFile); | ||
| /** | ||
| * Generic catalog entry for the `dep-advisory` rule. The per-advisory specifics | ||
| * (title / severity / patched version) live on each individual finding; this is | ||
| * the shared, package-agnostic description SARIF advertises for the rule. | ||
| */ | ||
| export const DEP_ADVISORY_RULE: RuleMeta = { | ||
| id: "dep-advisory", | ||
| title: "Dependency with a known security advisory", | ||
| category: "dependency", | ||
| // Representative default; each finding carries its own advisory severity. | ||
| severity: "high", | ||
| confidence: "high", | ||
| hndl: false, | ||
| message: | ||
| "A pinned dependency has a published security advisory (CVE / RUSTSEC / GHSA / PYSEC). Upgrade to a patched version.", | ||
| remediation: "Upgrade the affected package to the advisory's patched release.", | ||
| description: | ||
| "Flags dependencies with a known security advisory, via the ecosystem's own audit tool (cargo audit / pip-audit / npm audit). Opt-in with `qscan --audit`.", | ||
| }; | ||
| /** Options for {@link scanAdvisories}. */ | ||
| export interface ScanAdvisoriesOptions { | ||
| /** Per-tool timeout in milliseconds. Default: 120_000. */ | ||
| timeoutMs?: number; | ||
| /** Max stdout buffer per tool, in bytes. Default: 32 MiB. */ | ||
| maxBuffer?: number; | ||
| /** | ||
| * Injectable command runner (for tests). Resolves with the tool's stdout, or | ||
| * rejects with an error carrying `code` (e.g. `"ENOENT"`) and, for a non-zero | ||
| * exit, the captured `stdout`. Defaults to a promisified `execFile`. | ||
| */ | ||
| exec?: ExecFn; | ||
| /** Injectable directory lister (for tests). Defaults to `fs.readdir`. */ | ||
| listDir?: (dir: string) => Promise<string[]>; | ||
| } | ||
| /** Shape of an injectable command runner and of the errors it may reject with. */ | ||
| export type ExecFn = ( | ||
| command: string, | ||
| args: readonly string[], | ||
| options: { cwd: string; timeout: number; maxBuffer: number }, | ||
| ) => Promise<{ stdout: string; stderr: string }>; | ||
| interface ExecError { | ||
| code?: string; | ||
| killed?: boolean; | ||
| signal?: string; | ||
| stdout?: string; | ||
| stderr?: string; | ||
| message?: string; | ||
| } | ||
| const DEFAULT_TIMEOUT_MS = 120_000; | ||
| const DEFAULT_MAX_BUFFER = 32 * 1024 * 1024; | ||
| /** One audit tool and how to detect + parse it. */ | ||
| interface AuditTool { | ||
| /** Human label used in diagnostics (e.g. "cargo audit"). */ | ||
| label: string; | ||
| /** Program to run. */ | ||
| command: string; | ||
| /** Arguments (must request JSON output). */ | ||
| args: string[]; | ||
| /** | ||
| * Given the project's top-level entry names, return the manifest file the | ||
| * advisories should be located at, or null when this ecosystem is absent. | ||
| */ | ||
| manifest: (entries: readonly string[]) => string | null; | ||
| /** Parse the tool's JSON stdout into normalized advisory records. */ | ||
| parse: (json: unknown) => AdvisoryRecord[]; | ||
| } | ||
| /** A normalized advisory, ecosystem-independent. */ | ||
| interface AdvisoryRecord { | ||
| /** Advisory id (CVE-…, RUSTSEC-…, GHSA-…, PYSEC-…). */ | ||
| id: string; | ||
| package: string; | ||
| version: string; | ||
| summary: string; | ||
| severity: Severity; | ||
| /** Patched version(s), when the tool reports them. */ | ||
| patched?: string; | ||
| } | ||
| /** Map an ecosystem-reported severity token to our {@link Severity}. */ | ||
| function toSeverity(raw: unknown): Severity { | ||
| const s = String(raw ?? "").toLowerCase(); | ||
| if (s === "critical") return "critical"; | ||
| if (s === "high") return "high"; | ||
| if (s === "moderate" || s === "medium") return "medium"; | ||
| if (s === "low") return "low"; | ||
| if (s === "info" || s === "informational" || s === "none" || s === "negligible") return "info"; | ||
| // An advisory with no usable severity is treated as high (conservative — a | ||
| // known-vulnerable pinned dependency should not silently pass a scan). | ||
| return "high"; | ||
| } | ||
| function asRecord(v: unknown): Record<string, unknown> | null { | ||
| return v !== null && typeof v === "object" ? (v as Record<string, unknown>) : null; | ||
| } | ||
| function str(v: unknown): string { | ||
| return typeof v === "string" ? v : v === undefined || v === null ? "" : String(v); | ||
| } | ||
| function firstString(v: unknown): string | undefined { | ||
| if (typeof v === "string" && v) return v; | ||
| if (Array.isArray(v)) { | ||
| const s = v.find((x) => typeof x === "string" && x); | ||
| return typeof s === "string" ? s : undefined; | ||
| } | ||
| return undefined; | ||
| } | ||
| /** cargo audit --json → advisories. */ | ||
| function parseCargoAudit(json: unknown): AdvisoryRecord[] { | ||
| const root = asRecord(json); | ||
| const vulns = asRecord(root?.vulnerabilities); | ||
| const list = Array.isArray(vulns?.list) ? vulns.list : []; | ||
| const out: AdvisoryRecord[] = []; | ||
| for (const item of list) { | ||
| const rec = asRecord(item); | ||
| const advisory = asRecord(rec?.advisory); | ||
| const pkg = asRecord(rec?.package); | ||
| const versions = asRecord(rec?.versions); | ||
| if (!advisory) continue; | ||
| out.push({ | ||
| id: str(advisory.id) || "RUSTSEC-UNKNOWN", | ||
| package: str(pkg?.name) || str(advisory.package), | ||
| version: str(pkg?.version), | ||
| summary: str(advisory.title) || "security advisory", | ||
| severity: toSeverity(advisory.severity), | ||
| patched: firstString(versions?.patched), | ||
| }); | ||
| } | ||
| return out; | ||
| } | ||
| /** pip-audit --format json → advisories. Handles the object + legacy-array forms. */ | ||
| function parsePipAudit(json: unknown): AdvisoryRecord[] { | ||
| const root = asRecord(json); | ||
| const deps = Array.isArray(json) | ||
| ? json | ||
| : Array.isArray(root?.dependencies) | ||
| ? root.dependencies | ||
| : []; | ||
| const out: AdvisoryRecord[] = []; | ||
| for (const item of deps) { | ||
| const dep = asRecord(item); | ||
| if (!dep) continue; | ||
| const name = str(dep.name); | ||
| const version = str(dep.version); | ||
| const vulns = Array.isArray(dep.vulns) ? dep.vulns : []; | ||
| for (const v of vulns) { | ||
| const vuln = asRecord(v); | ||
| if (!vuln) continue; | ||
| out.push({ | ||
| id: str(vuln.id) || firstString(vuln.aliases) || "PYSEC-UNKNOWN", | ||
| package: name, | ||
| version, | ||
| summary: str(vuln.description) || "security advisory", | ||
| // pip-audit's base JSON does not grade severity; treat as high. | ||
| severity: toSeverity(vuln.severity), | ||
| patched: firstString(vuln.fix_versions), | ||
| }); | ||
| } | ||
| } | ||
| return out; | ||
| } | ||
| /** Extract a GHSA id from an advisory URL when present. */ | ||
| function ghsaFrom(url: string): string | undefined { | ||
| const m = /GHSA-[\w-]+/.exec(url); | ||
| return m ? m[0] : undefined; | ||
| } | ||
| /** npm audit --json (npm v7+) → advisories. */ | ||
| function parseNpmAudit(json: unknown): AdvisoryRecord[] { | ||
| const root = asRecord(json); | ||
| const vulns = asRecord(root?.vulnerabilities); | ||
| if (!vulns) return []; | ||
| const out: AdvisoryRecord[] = []; | ||
| for (const [name, entry] of Object.entries(vulns)) { | ||
| const rec = asRecord(entry); | ||
| if (!rec) continue; | ||
| const range = str(rec.range); | ||
| const fix = asRecord(rec.fixAvailable); | ||
| const patched = fix ? str(fix.version) : undefined; | ||
| const via = Array.isArray(rec.via) ? rec.via : []; | ||
| for (const v of via) { | ||
| const adv = asRecord(v); | ||
| if (!adv) continue; // string `via` = transitive; the source entry carries the detail. | ||
| const url = str(adv.url); | ||
| const id = | ||
| ghsaFrom(url) || | ||
| (adv.source !== undefined ? `npm-advisory-${str(adv.source)}` : url) || | ||
| "npm-advisory"; | ||
| out.push({ | ||
| id, | ||
| package: str(adv.name) || name, | ||
| version: range, | ||
| summary: str(adv.title) || "security advisory", | ||
| severity: toSeverity(adv.severity ?? rec.severity), | ||
| patched: patched || undefined, | ||
| }); | ||
| } | ||
| } | ||
| return out; | ||
| } | ||
| /** Match `requirements*.txt` (requirements.txt, requirements-dev.txt, …). */ | ||
| function isRequirements(name: string): boolean { | ||
| return /^requirements[\w.-]*\.txt$/i.test(name); | ||
| } | ||
| /** The audit tools, in a deterministic order. */ | ||
| const AUDIT_TOOLS: readonly AuditTool[] = [ | ||
| { | ||
| label: "cargo audit", | ||
| command: "cargo", | ||
| args: ["audit", "--json"], | ||
| manifest: (e) => | ||
| e.includes("Cargo.lock") ? "Cargo.lock" : e.includes("Cargo.toml") ? "Cargo.toml" : null, | ||
| parse: parseCargoAudit, | ||
| }, | ||
| { | ||
| label: "pip-audit", | ||
| command: "pip-audit", | ||
| args: ["--format", "json"], | ||
| manifest: (e) => { | ||
| const req = e.find(isRequirements); | ||
| if (req) return req; | ||
| return e.includes("pyproject.toml") ? "pyproject.toml" : null; | ||
| }, | ||
| parse: parsePipAudit, | ||
| }, | ||
| { | ||
| label: "npm audit", | ||
| command: "npm", | ||
| args: ["audit", "--json"], | ||
| manifest: (e) => (e.includes("package-lock.json") ? "package-lock.json" : null), | ||
| parse: parseNpmAudit, | ||
| }, | ||
| ]; | ||
| /** Build a {@link Finding} from a normalized advisory record. */ | ||
| function advisoryFinding(rec: AdvisoryRecord, manifest: string): Finding { | ||
| const pkgVer = rec.version ? `${rec.package}@${rec.version}` : rec.package; | ||
| const finding: Finding = { | ||
| ruleId: "dep-advisory", | ||
| title: rec.id, | ||
| category: "dependency", | ||
| severity: rec.severity, | ||
| confidence: "high", | ||
| hndl: false, | ||
| message: `${pkgVer}: ${rec.summary} (${rec.id})`, | ||
| location: { file: manifest, line: 1 }, | ||
| }; | ||
| if (rec.patched) finding.remediation = `Upgrade ${rec.package} to ${rec.patched}`; | ||
| return finding; | ||
| } | ||
| /** | ||
| * Scan `root` for dependency security advisories by shelling out to each present | ||
| * ecosystem's audit tool. Never throws: a missing tool or a tool error becomes a | ||
| * diagnostic string. Returns the merged findings plus the diagnostics. | ||
| */ | ||
| export async function scanAdvisories( | ||
| root: string, | ||
| opts: ScanAdvisoriesOptions = {}, | ||
| ): Promise<{ findings: Finding[]; diagnostics: string[] }> { | ||
| const timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS; | ||
| const maxBuffer = opts.maxBuffer ?? DEFAULT_MAX_BUFFER; | ||
| const list = opts.listDir ?? ((dir: string) => readdir(dir)); | ||
| const exec: ExecFn = | ||
| opts.exec ?? | ||
| ((command, args, options) => | ||
| execFileAsync(command, args as string[], { ...options, windowsHide: true })); | ||
| const findings: Finding[] = []; | ||
| const diagnostics: string[] = []; | ||
| let entries: string[]; | ||
| try { | ||
| entries = await list(root); | ||
| } catch { | ||
| return { findings, diagnostics }; // unreadable root — nothing to audit. | ||
| } | ||
| for (const tool of AUDIT_TOOLS) { | ||
| const manifest = tool.manifest(entries); | ||
| if (manifest === null) continue; // this ecosystem isn't present. | ||
| let stdout: string; | ||
| try { | ||
| const res = await exec(tool.command, tool.args, { cwd: root, timeout, maxBuffer }); | ||
| stdout = res.stdout; | ||
| } catch (err) { | ||
| const e = err as ExecError; | ||
| if (e.code === "ENOENT") { | ||
| diagnostics.push(`${tool.label} not available, skipped`); | ||
| continue; | ||
| } | ||
| if (e.killed || e.signal === "SIGTERM") { | ||
| diagnostics.push(`${tool.label} timed out, skipped`); | ||
| continue; | ||
| } | ||
| // A non-zero exit is EXPECTED when advisories are found: the JSON is on | ||
| // the error's stdout. Only when there is no parseable stdout is it a real | ||
| // failure we skip over. | ||
| if (typeof e.stdout === "string" && e.stdout.trim()) { | ||
| stdout = e.stdout; | ||
| } else { | ||
| const detail = (e.stderr || e.message || "").trim().slice(0, 160); | ||
| diagnostics.push(`${tool.label} failed, skipped${detail ? `: ${detail}` : ""}`); | ||
| continue; | ||
| } | ||
| } | ||
| let json: unknown; | ||
| try { | ||
| json = JSON.parse(stdout); | ||
| } catch { | ||
| diagnostics.push(`${tool.label} produced unparseable output, skipped`); | ||
| continue; | ||
| } | ||
| let records: AdvisoryRecord[]; | ||
| try { | ||
| records = tool.parse(json); | ||
| } catch { | ||
| diagnostics.push(`${tool.label} output could not be interpreted, skipped`); | ||
| continue; | ||
| } | ||
| // Dedupe by advisory id + package (npm lists the same advisory under both a | ||
| // direct and a transitive path). | ||
| const seen = new Set<string>(); | ||
| for (const rec of records) { | ||
| const key = `${rec.id}|${rec.package}`; | ||
| if (seen.has(key)) continue; | ||
| seen.add(key); | ||
| findings.push(advisoryFinding(rec, path.posix.basename(manifest))); | ||
| } | ||
| } | ||
| return { findings, diagnostics }; | ||
| } |
| /** | ||
| * Config/any-scope detector: post-quantum KEM parameter / size sanity checks. | ||
| * | ||
| * WHY THIS LIVES IN A PQC-READINESS TOOL. | ||
| * The rest of qScan flags *classical* crypto that must migrate to PQC. This | ||
| * detector is the mirror image: it inspects code that has ALREADY reached for a | ||
| * post-quantum KEM and flags two ways that migration can be quietly wrong — | ||
| * using a *pre-standard* Kyber while claiming FIPS 203, and an internally | ||
| * *inconsistent* parameter set (a byte size that names one ML-KEM/Kyber level | ||
| * while the code advertises a different one). Neither is "quantum-broken", but | ||
| * both defeat the point of migrating: a round-3 Kyber is not interoperable with | ||
| * (nor validated as) FIPS 203 ML-KEM, and a size/level mismatch is a latent bug | ||
| * or a mislabelled security claim. Both rules are `category: "kem"`, | ||
| * `hndl: false` (this is about a PQC primitive's correctness, not a classical | ||
| * confidentiality secret exposed to harvest-now-decrypt-later). | ||
| * | ||
| * RULE 1 — `pqc-prestandard-kem` (medium). | ||
| * Curated identifiers for the well-known PRE-FIPS-203, round-3 CRYSTALS-Kyber | ||
| * packages and APIs (the `pqc_kyber` / `pqcrypto-kyber` / `safe_pqc_kyber` / | ||
| * `crystals-kyber` crates & npm packages, the reference `crypto_kem_kyber768_*` | ||
| * / `pqcrystals_kyber*` API, and the round-3 `Kyber512/768/1024` parameter | ||
| * names). These are NOT ML-KEM: FIPS 203 changed the KDF/domain separation and | ||
| * the algorithm names, so a `Kyber768` build is not a `ML-KEM-768` build. The | ||
| * finding names the exact identifier matched. Confidence starts LOW (using a | ||
| * round-3 Kyber may be a deliberate, documented choice) and is RAISED when the | ||
| * same file also makes a FIPS-203 / ML-KEM / NIST claim — the strong signal that | ||
| * pre-standard Kyber is being passed off as the standard. | ||
| * | ||
| * RULE 2 — `pqc-parameter-mismatch` (medium, LOWER confidence, deliberately | ||
| * conservative). A curated table of the distinctive ML-KEM / Kyber byte sizes: | ||
| * 512 → pk 800, sk 1632 (ct 768 is intentionally omitted: it | ||
| * collides with the level number 768) | ||
| * 768 → pk 1184, sk 2400, ct 1088 | ||
| * 1024 → pk 1568, sk 3168 (pk and ct are both 1568) | ||
| * The rule fires ONLY when a file (already in a Kyber / ML-KEM context) contains | ||
| * one of these exact sizes as a standalone integer AND advertises a DIFFERENT | ||
| * parameter level in text (`ML-KEM-1024`, `Kyber-1024`, …) AND does NOT also | ||
| * advertise the size's own level. So `pk = 1184` (an ML-KEM-768 public key) in a | ||
| * file that calls itself `ML-KEM-1024` fires; a file that mentions both 768 and | ||
| * 1024 (a multi-parameter module) stays silent. Otherwise silent. | ||
| */ | ||
| import type { Detector, Finding, RuleMeta } from "../types.js"; | ||
| import { | ||
| DOC_EXTENSIONS, | ||
| eachMatch, | ||
| findingFromRule, | ||
| hasExtension, | ||
| maskBlockComments, | ||
| maskCommentLines, | ||
| } from "../detect-utils.js"; | ||
| // --- Rule 1: pre-standard round-3 Kyber identifiers ----------------------------- | ||
| // Package / crate names for the round-3 CRYSTALS-Kyber implementations that | ||
| // predate FIPS 203 ML-KEM (Rust crates, npm packages, Python bindings). | ||
| const RE_KYBER_PKG = | ||
| /\b(?:pqc[_-]?kyber|pqcrypto[_-]kyber|safe_pqc_kyber|crystals[_-]kyber|kyber[_-]crystals|py[_-]?kyber)\b/gi; | ||
| // The reference-implementation C API and its language wrappers. | ||
| const RE_KYBER_API = /\b(?:crypto_kem_kyber(?:512|768|1024)|pqcrystals_kyber(?:512|768|1024))\w*/gi; | ||
| // Round-3 parameter names — distinct from the FIPS 203 `ML-KEM-512/768/1024`. | ||
| const RE_KYBER_PARAM = /\bKyber-?(?:512|768|1024)\b/gi; | ||
| // The generic project name. | ||
| const RE_CRYSTALS_KYBER = /\bCRYSTALS[_-]?Kyber\b/gi; | ||
| const PRESTANDARD_RES: readonly RegExp[] = [ | ||
| RE_KYBER_PKG, | ||
| RE_KYBER_API, | ||
| RE_KYBER_PARAM, | ||
| RE_CRYSTALS_KYBER, | ||
| ]; | ||
| /** A FIPS-203 / ML-KEM / NIST claim in the same file — the confidence booster. */ | ||
| const RE_STANDARD_CLAIM = /\bFIPS[\s-]?203\b|\bML-?KEM\b|\bNIST\b/i; | ||
| const RULE_PRESTANDARD: RuleMeta = { | ||
| id: "pqc-prestandard-kem", | ||
| title: "Pre-standard (round-3) Kyber, not FIPS 203 ML-KEM", | ||
| description: | ||
| "A pre-FIPS-203, round-3 CRYSTALS-Kyber package/parameter set used where FIPS 203 ML-KEM is intended", | ||
| category: "kem", | ||
| severity: "medium", | ||
| confidence: "low", | ||
| algorithm: "unknown", | ||
| hndl: false, | ||
| message: | ||
| "Uses pre-standard round-3 Kyber; this is not FIPS 203 ML-KEM (different KDF/domain separation and not interoperable). Migrate to a FIPS 203 ML-KEM implementation.", | ||
| remediation: | ||
| "Replace the round-3 Kyber dependency with a FIPS 203 ML-KEM implementation (e.g. ML-KEM-768 / hybrid X25519MLKEM768) and re-run KATs against the FIPS 203 vectors.", | ||
| }; | ||
| // --- Rule 2: ML-KEM / Kyber size ↔ parameter-level mismatch ---------------------- | ||
| /** Distinctive ML-KEM/Kyber byte sizes → the parameter level they belong to. */ | ||
| const SIZE_TO_LEVEL: ReadonlyMap<number, 512 | 768 | 1024> = new Map([ | ||
| [800, 512], | ||
| [1632, 512], | ||
| [1184, 768], | ||
| [2400, 768], | ||
| [1088, 768], | ||
| [1568, 1024], | ||
| [3168, 1024], | ||
| ]); | ||
| // Standalone integer tokens for the distinctive sizes (never inside a longer | ||
| // number or a decimal), so `11840` / `1.088` don't match. | ||
| const RE_SIZE = /(?<![\d.])(800|1632|1184|2400|1088|1568|3168)(?![\d.])/g; | ||
| // An advertised parameter level: `ML-KEM-768`, `MLKEM768`, `Kyber-768`, `Kyber768`. | ||
| const RE_ADVERTISED_LEVEL = /(?:ML-?KEM|Kyber)-?(512|768|1024)\b/gi; | ||
| const RULE_MISMATCH: RuleMeta = { | ||
| id: "pqc-parameter-mismatch", | ||
| title: "ML-KEM/Kyber size does not match the advertised parameter set", | ||
| description: | ||
| "A ML-KEM/Kyber key or ciphertext byte size names one parameter level while the code advertises a different one", | ||
| category: "kem", | ||
| severity: "medium", | ||
| confidence: "low", | ||
| algorithm: "unknown", | ||
| hndl: false, | ||
| message: | ||
| "A ML-KEM/Kyber byte size does not match the advertised parameter set — likely a mislabelled security level or a copied constant.", | ||
| }; | ||
| /** Collect the distinct advertised parameter levels named in `content`. */ | ||
| function advertisedLevels(content: string): Set<number> { | ||
| const levels = new Set<number>(); | ||
| eachMatch(RE_ADVERTISED_LEVEL, content, (m) => levels.add(Number.parseInt(m[1], 10))); | ||
| return levels; | ||
| } | ||
| /** | ||
| * Detector: post-quantum KEM parameter / size checks. Fast-rejects any file that | ||
| * does not mention Kyber or ML-KEM at all, so it never touches ordinary code. | ||
| */ | ||
| export const pqcParameterDetector: Detector = { | ||
| id: "pqc-parameter", | ||
| description: | ||
| "Post-quantum KEM parameter checks: pre-standard round-3 Kyber, and ML-KEM/Kyber size ↔ parameter-set mismatch", | ||
| scope: "config", | ||
| language: "any", | ||
| rules: [RULE_PRESTANDARD, RULE_MISMATCH], | ||
| // Skip prose/docs: a design note discussing Kyber is not live code. | ||
| appliesTo: (f) => !hasExtension(f, DOC_EXTENSIONS), | ||
| detect({ file, content }): Finding[] { | ||
| // Fast reject: only files that actually reach for a Kyber / ML-KEM KEM. | ||
| if (!/kyber|ml-?kem/i.test(content)) return []; | ||
| // Mask comments so a commented-out identifier or a migration note can't fire. | ||
| const scan = maskCommentLines(maskBlockComments(content), ["//", "#", ";"]); | ||
| const findings: Finding[] = []; | ||
| // Rule 1 — pre-standard round-3 Kyber identifiers. The FIPS-203/ML-KEM/NIST | ||
| // *claim* is read from the ORIGINAL content (not the comment-masked copy): a | ||
| // "FIPS 203 compliant" claim in a comment/docstring is exactly the mislabel | ||
| // signal we want to boost confidence on. | ||
| const claimPresent = RE_STANDARD_CLAIM.test(content); | ||
| for (const re of PRESTANDARD_RES) { | ||
| eachMatch(re, scan, (m) => { | ||
| const id = m[0]; | ||
| findings.push( | ||
| findingFromRule( | ||
| RULE_PRESTANDARD, | ||
| { file, content, index: m.index, matchLength: id.length }, | ||
| { | ||
| // A co-located FIPS-203/ML-KEM/NIST claim is the strong signal that | ||
| // pre-standard Kyber is being passed off as the standard. | ||
| confidence: claimPresent ? "high" : "low", | ||
| message: `Uses pre-standard round-3 Kyber (${id}); not FIPS 203 ML-KEM${ | ||
| claimPresent ? " despite a FIPS-203/ML-KEM/NIST claim in the same file" : "" | ||
| }. Migrate to a FIPS 203 ML-KEM implementation.`, | ||
| }, | ||
| ), | ||
| ); | ||
| }); | ||
| } | ||
| // Rule 2 — size ↔ advertised-level mismatch (conservative). The advertised | ||
| // parameter level is read from the ORIGINAL content, since it is very often a | ||
| // comment/docstring label (`// ML-KEM-1024 key sizes`) sitting above the | ||
| // constant. The SIZE token itself is read from the masked copy, so a | ||
| // commented-out constant can't fire. Only meaningful when a level is advertised. | ||
| const advertised = advertisedLevels(content); | ||
| if (advertised.size > 0) { | ||
| // Dedupe by (size,line) so the same constant isn't reported twice. | ||
| const seen = new Set<string>(); | ||
| eachMatch(RE_SIZE, scan, (m) => { | ||
| const size = Number.parseInt(m[0], 10); | ||
| const level = SIZE_TO_LEVEL.get(size); | ||
| if (level === undefined) return; | ||
| // Silent when the size's own level is also advertised (a multi-parameter | ||
| // file), or when nothing conflicting is advertised. | ||
| if (advertised.has(level)) return; | ||
| const conflict = [...advertised].find((l) => l !== level); | ||
| if (conflict === undefined) return; | ||
| const key = `${size}:${m.index}`; | ||
| if (seen.has(key)) return; | ||
| seen.add(key); | ||
| findings.push( | ||
| findingFromRule( | ||
| RULE_MISMATCH, | ||
| { file, content, index: m.index, matchLength: m[0].length }, | ||
| { | ||
| message: `Byte size ${size} matches ML-KEM-${level} but the code advertises ML-KEM-${conflict}; parameter-set mismatch (mislabelled level or a copied constant).`, | ||
| }, | ||
| ), | ||
| ); | ||
| }); | ||
| } | ||
| return findings; | ||
| }, | ||
| }; |
| /** | ||
| * Provenance / declared-source-repository check (wired to `qscan --audit`). | ||
| * | ||
| * A package that declares no source repository, or one whose declared repository | ||
| * does not resolve, cannot have its published artifacts verified against source — | ||
| * a supply-chain gap. This reads the ROOT manifest's repository URL and emits: | ||
| * | ||
| * - `provenance-repo-missing` (info) — the manifest declares no repository. | ||
| * - `provenance-repo-unresolved`(medium) — the declared repository 404s / does | ||
| * not resolve (network mode only). | ||
| * | ||
| * OFFLINE BOUNDARY (ADR-0005). `@quantakrypto/core` must stay strictly offline: it | ||
| * may NOT import `node:https` or make outbound calls. So this module does the | ||
| * pure work — parse the manifest, decide what to check — and delegates the actual | ||
| * HEAD request to an INJECTED {@link RepoHeadRequester} that the caller (qScan, | ||
| * which is allowed to be networked) supplies. In the default (static) mode no | ||
| * network hook is used at all, so a scan without `--audit` never reaches for one. | ||
| */ | ||
| import { readFile } from "node:fs/promises"; | ||
| import * as path from "node:path"; | ||
| import type { Finding } from "./types.js"; | ||
| /** Outcome of a HEAD request to a declared repository URL. */ | ||
| export type RepoHeadOutcome = | ||
| | { kind: "status"; status: number } // an HTTP response was received. | ||
| | { kind: "unresolved" } // DNS / host-not-found — the repository does not exist. | ||
| | { kind: "error"; message: string }; // transient network error — verification skipped. | ||
| /** | ||
| * Injected HEAD requester. Implemented by the (networked) caller — qScan supplies | ||
| * a `node:https`-backed one — so core never imports an outbound network module. | ||
| * MUST resolve (never reject); map its own failures onto {@link RepoHeadOutcome}. | ||
| */ | ||
| export type RepoHeadRequester = (url: string, timeoutMs: number) => Promise<RepoHeadOutcome>; | ||
| /** Options for {@link checkProvenance}. */ | ||
| export interface ProvenanceOptions { | ||
| /** | ||
| * Verify the declared repository over the network via {@link head}. When false | ||
| * (or when no `head` is supplied) only the static `repo-missing` check runs. | ||
| */ | ||
| network?: boolean; | ||
| /** HEAD-request timeout in milliseconds. Default: 5000. */ | ||
| timeoutMs?: number; | ||
| /** Injected HEAD requester (required for the network check to do anything). */ | ||
| head?: RepoHeadRequester; | ||
| /** Injected file reader (for tests). Defaults to `fs.readFile`. */ | ||
| readManifest?: (file: string) => Promise<string>; | ||
| } | ||
| const DEFAULT_TIMEOUT_MS = 5000; | ||
| /** The root manifests we understand, in precedence order. */ | ||
| const MANIFESTS = ["package.json", "Cargo.toml", "pyproject.toml"] as const; | ||
| /** What a root manifest told us about its declared repository. */ | ||
| interface ManifestRepo { | ||
| /** The manifest filename that was read. */ | ||
| file: string; | ||
| /** The declared repository URL, or null when the manifest declares none. */ | ||
| url: string | null; | ||
| } | ||
| /** | ||
| * Normalize a declared repository reference to an https(s) URL, or null when it | ||
| * cannot be turned into one we can HEAD. Handles `git+https://…`, `git://…`, | ||
| * `git@github.com:owner/repo(.git)`, the `owner/repo` and `github:owner/repo` | ||
| * shorthands, and strips a trailing `.git`. | ||
| */ | ||
| export function normalizeRepoUrl(raw: string): string | null { | ||
| let s = raw.trim(); | ||
| if (!s) return null; | ||
| s = s.replace(/^git\+/, ""); | ||
| // scp-style `git@host:owner/repo` | ||
| const scp = /^[\w.-]+@([\w.-]+):(.+)$/.exec(s); | ||
| if (scp) s = `https://${scp[1]}/${scp[2]}`; | ||
| if (s.startsWith("git://")) s = `https://${s.slice("git://".length)}`; | ||
| if (s.startsWith("ssh://")) s = `https://${s.slice("ssh://".length)}`; | ||
| // `github:owner/repo` / `gitlab:owner/repo` / `bitbucket:owner/repo` | ||
| const hosted = /^(github|gitlab|bitbucket):(.+)$/.exec(s); | ||
| if (hosted) { | ||
| const host = hosted[1] === "github" ? "github.com" : `${hosted[1]}.org`; | ||
| s = `https://${host}/${hosted[2]}`; | ||
| } | ||
| // bare `owner/repo` shorthand → GitHub | ||
| if (/^[\w.-]+\/[\w.-]+$/.test(s)) s = `https://github.com/${s}`; | ||
| if (!/^https?:\/\//i.test(s)) return null; | ||
| return s.replace(/\.git$/, ""); | ||
| } | ||
| /** package.json `repository` (string or `{ url }`). */ | ||
| function repoFromPackageJson(content: string): string | null { | ||
| let json: unknown; | ||
| try { | ||
| json = JSON.parse(content); | ||
| } catch { | ||
| return null; | ||
| } | ||
| if (json === null || typeof json !== "object") return null; | ||
| const repo = (json as Record<string, unknown>).repository; | ||
| if (typeof repo === "string") return normalizeRepoUrl(repo); | ||
| if (repo !== null && typeof repo === "object") { | ||
| const url = (repo as Record<string, unknown>).url; | ||
| if (typeof url === "string") return normalizeRepoUrl(url); | ||
| } | ||
| return null; | ||
| } | ||
| /** Cargo.toml `[package] repository = "…"`. */ | ||
| function repoFromCargoToml(content: string): string | null { | ||
| const m = /^\s*repository\s*=\s*"([^"]+)"/m.exec(content); | ||
| return m ? normalizeRepoUrl(m[1]) : null; | ||
| } | ||
| /** | ||
| * pyproject.toml — `[project.urls]` `Repository`/`Source`/`Homepage`, or the | ||
| * legacy `[tool.poetry]` `repository = "…"`. Generous line scan (any of those | ||
| * keys → url), which is enough to know a repository was declared. | ||
| */ | ||
| function repoFromPyproject(content: string): string | null { | ||
| const m = /^\s*(?:repository|source|homepage)\s*=\s*"([^"]+)"/im.exec(content); | ||
| return m ? normalizeRepoUrl(m[1]) : null; | ||
| } | ||
| const PARSERS: Record<(typeof MANIFESTS)[number], (content: string) => string | null> = { | ||
| "package.json": repoFromPackageJson, | ||
| "Cargo.toml": repoFromCargoToml, | ||
| "pyproject.toml": repoFromPyproject, | ||
| }; | ||
| /** | ||
| * Read the first present root manifest and extract its declared repository URL. | ||
| * Returns null when NO root manifest exists (there is no package to assess). | ||
| */ | ||
| async function readManifestRepo( | ||
| root: string, | ||
| read: (file: string) => Promise<string>, | ||
| ): Promise<ManifestRepo | null> { | ||
| for (const file of MANIFESTS) { | ||
| let content: string; | ||
| try { | ||
| content = await read(path.join(root, file)); | ||
| } catch { | ||
| continue; // manifest absent / unreadable — try the next. | ||
| } | ||
| return { file, url: PARSERS[file](content) }; | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Check a project's declared-source-repository provenance. | ||
| * | ||
| * Static (always): a present root manifest that declares NO repository yields a | ||
| * `provenance-repo-missing` info finding. Network (`network: true` + a `head` | ||
| * requester): a declared repository that 404s or does not resolve yields a | ||
| * `provenance-repo-unresolved` (medium) finding; transient network errors are | ||
| * skipped silently (recorded as a diagnostic). Never throws. | ||
| */ | ||
| export async function checkProvenance( | ||
| root: string, | ||
| opts: ProvenanceOptions = {}, | ||
| ): Promise<{ findings: Finding[]; diagnostics: string[] }> { | ||
| const read = opts.readManifest ?? ((file: string) => readFile(file, "utf8")); | ||
| const findings: Finding[] = []; | ||
| const diagnostics: string[] = []; | ||
| const manifest = await readManifestRepo(root, read); | ||
| if (manifest === null) return { findings, diagnostics }; // no manifest → nothing to assess. | ||
| if (manifest.url === null) { | ||
| findings.push({ | ||
| ruleId: "provenance-repo-missing", | ||
| title: "Package declares no source repository", | ||
| category: "dependency", | ||
| severity: "info", | ||
| confidence: "medium", | ||
| hndl: false, | ||
| message: "Package declares no source repository; builds cannot be verified against source.", | ||
| remediation: | ||
| "Declare a source repository in the manifest (package.json `repository`, Cargo.toml `repository`, or pyproject.toml `[project.urls]`).", | ||
| location: { file: path.posix.basename(manifest.file), line: 1 }, | ||
| }); | ||
| return { findings, diagnostics }; | ||
| } | ||
| // Network verification is opt-in and needs an injected requester. | ||
| if (!opts.network || !opts.head) return { findings, diagnostics }; | ||
| const timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS; | ||
| let outcome: RepoHeadOutcome; | ||
| try { | ||
| outcome = await opts.head(manifest.url, timeout); | ||
| } catch (err) { | ||
| const message = err instanceof Error ? err.message : String(err); | ||
| diagnostics.push(`provenance: could not verify ${manifest.url} (${message}), skipped`); | ||
| return { findings, diagnostics }; | ||
| } | ||
| const unresolved = | ||
| outcome.kind === "unresolved" || | ||
| (outcome.kind === "status" && (outcome.status === 404 || outcome.status === 410)); | ||
| if (unresolved) { | ||
| findings.push({ | ||
| ruleId: "provenance-repo-unresolved", | ||
| title: "Declared source repository does not resolve", | ||
| category: "dependency", | ||
| severity: "medium", | ||
| confidence: "medium", | ||
| hndl: false, | ||
| message: `Declared source repository ${manifest.url} does not resolve.`, | ||
| remediation: | ||
| "Fix the manifest's repository URL to point at the real, reachable source repository.", | ||
| location: { file: path.posix.basename(manifest.file), line: 1 }, | ||
| }); | ||
| } else if (outcome.kind === "error") { | ||
| diagnostics.push(`provenance: could not verify ${manifest.url} (${outcome.message}), skipped`); | ||
| } | ||
| return { findings, diagnostics }; | ||
| } | ||
| /** The two rule ids this module can emit, for SARIF catalog registration. */ | ||
| export const PROVENANCE_RULES: import("./types.js").RuleMeta[] = [ | ||
| { | ||
| id: "provenance-repo-missing", | ||
| title: "Package declares no source repository", | ||
| category: "dependency", | ||
| severity: "info", | ||
| confidence: "medium", | ||
| hndl: false, | ||
| message: "Package declares no source repository; builds cannot be verified against source.", | ||
| description: "The root manifest declares no source repository (opt-in with `qscan --audit`).", | ||
| }, | ||
| { | ||
| id: "provenance-repo-unresolved", | ||
| title: "Declared source repository does not resolve", | ||
| category: "dependency", | ||
| severity: "medium", | ||
| confidence: "medium", | ||
| hndl: false, | ||
| message: "The declared source repository does not resolve (404 / DNS failure).", | ||
| description: | ||
| "The manifest's declared source repository 404s or does not resolve (opt-in with `qscan --audit`).", | ||
| }, | ||
| ]; |
+4
-0
@@ -45,2 +45,6 @@ /** | ||
| export { vulnerableDependencies, DEP_VULNERABLE_RULE, isManifestFile } from "./dependencies.js"; | ||
| export { scanAdvisories, DEP_ADVISORY_RULE } from "./advisories.js"; | ||
| export type { ScanAdvisoriesOptions, ExecFn } from "./advisories.js"; | ||
| export { checkProvenance, normalizeRepoUrl, PROVENANCE_RULES } from "./provenance.js"; | ||
| export type { ProvenanceOptions, RepoHeadRequester, RepoHeadOutcome } from "./provenance.js"; | ||
| export { SEVERITY_ORDER, severityRank, meetsThreshold, sarifLevel } from "./severity.js"; | ||
@@ -47,0 +51,0 @@ export { toSarif, toJson, formatSummary, formatTierGuidance, formatProfileGuidance, } from "./report.js"; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,cAAc,YAAY,CAAC;AAG3B,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAGvC,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAG5C,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAGzE,OAAO,EAAE,SAAS,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAC7D,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD,YAAY,EACV,YAAY,EACZ,eAAe,EACf,aAAa,EACb,KAAK,EACL,WAAW,GACZ,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACvF,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,qBAAqB,GACtB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACrE,YAAY,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,YAAY,EACV,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,aAAa,GACd,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAI9D,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,YAAY,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAI/C,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGlE,OAAO,EACL,kBAAkB,EAClB,oBAAoB,EACpB,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,gBAAgB,GACjB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAG9C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAG5C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACvE,YAAY,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAG5E,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,sBAAsB,EACtB,iBAAiB,EACjB,0BAA0B,EAC1B,iBAAiB,EACjB,4BAA4B,EAC5B,+BAA+B,EAC/B,8BAA8B,EAC9B,SAAS,EACT,WAAW,EACX,mBAAmB,EACnB,WAAW,EACX,SAAS,EACT,kBAAkB,EAClB,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,gBAAgB,GACjB,MAAM,WAAW,CAAC;AACnB,YAAY,EACV,kBAAkB,EAClB,SAAS,EACT,aAAa,EACb,WAAW,EACX,YAAY,EACZ,OAAO,EACP,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,WAAW,EACX,UAAU,GACX,MAAM,WAAW,CAAC;AAGnB,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAGnE,OAAO,EACL,kBAAkB,EAClB,4BAA4B,EAC5B,0BAA0B,GAC3B,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAKhD,OAAO,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAKhG,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpE,YAAY,EAAE,qBAAqB,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAKrE,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACtF,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAG7F,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGzF,OAAO,EACL,OAAO,EACP,MAAM,EACN,aAAa,EACb,kBAAkB,EAClB,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGjD,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACnC,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE7D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAElF,OAAO,EACL,0BAA0B,EAC1B,6BAA6B,EAC7B,+BAA+B,EAC/B,8BAA8B,GAC/B,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,qBAAqB,EACrB,4BAA4B,EAC5B,oBAAoB,EACpB,wBAAwB,EACxB,mBAAmB,EACnB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AACjG,YAAY,EACV,eAAe,EACf,eAAe,EACf,sBAAsB,EACtB,cAAc,EACd,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,eAAe,CAAC;AAGvB,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACpE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAGpG,OAAO,EACL,QAAQ,EACR,UAAU,EACV,UAAU,EACV,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,OAAO,EACP,WAAW,EACX,eAAe,EACf,aAAa,EACb,qBAAqB,EACrB,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,qBAAqB,EACrB,WAAW,EACX,iBAAiB,EACjB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAGrD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAG7F,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,mBAAmB,EACnB,uBAAuB,GACxB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAG9E,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,UAAU,CAAC"} |
+8
-0
@@ -51,2 +51,10 @@ /** | ||
| export { vulnerableDependencies, DEP_VULNERABLE_RULE, isManifestFile } from "./dependencies.js"; | ||
| // Dependency-advisory scanning (opt-in via `qscan --audit`): shells out to each | ||
| // ecosystem's own audit tool. `DEP_ADVISORY_RULE` is the generic SARIF catalog | ||
| // entry (advisory findings don't come from a Detector). | ||
| export { scanAdvisories, DEP_ADVISORY_RULE } from "./advisories.js"; | ||
| // Provenance / declared-source-repository check (opt-in via `qscan --audit`). The | ||
| // network HEAD request is INJECTED by the (networked) caller so core stays | ||
| // offline (ADR-0005). `PROVENANCE_RULES` are its generic SARIF catalog entries. | ||
| export { checkProvenance, normalizeRepoUrl, PROVENANCE_RULES } from "./provenance.js"; | ||
| // Severity utilities (ordering, threshold, SARIF level) — shared across tools. | ||
@@ -53,0 +61,0 @@ export { SEVERITY_ORDER, severityRank, meetsThreshold, sarifLevel } from "./severity.js"; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @quantakrypto/core — public API (LOCKED CONTRACT).\n *\n * The exported NAMES and SIGNATURES below are the stable contract that\n * @quantakrypto/qscan, @quantakrypto/mcp and the GitHub Action depend on — do not change\n * them without updating all consumers. The implementations live in focused\n * modules under src/ and are re-exported here; the public surface is identical\n * to the original stub file.\n */\nexport * from \"./types.js\";\n\n// Tool version, surfaced in reports. Keep in sync with package.json.\nexport { VERSION } from \"./version.js\";\n\n// Minimal SARIF 2.1.0 log shape, defined alongside the reporters.\nexport type { SarifLog } from \"./report.js\";\n\n// Core orchestration + built-in detector set.\nexport { scan, detectors, detectFile, compareFindings } from \"./scan.js\";\n\n// Snippet-level fix verification (shared by MCP verify_fix + remediation).\nexport { verifyFix, languageToExtension } from \"./verify.js\";\nexport type { VerifyResult } from \"./verify.js\";\n\n// Agent-plane shared types + the context redactor (offline; reused by MCP).\nexport type {\n ContextLevel,\n RedactedContext,\n TriageVerdict,\n Patch,\n FixProposal,\n} from \"./agent-types.js\";\nexport { buildContext, renderPreflight } from \"./redact.js\";\nexport { TRIAGE_RUBRIC, TRIAGE_VERDICT_SCHEMA, buildTriageRequest } from \"./triage.js\";\nexport type { TriageRequest } from \"./triage.js\";\nexport {\n REMEDIATE_RUBRIC,\n FIX_REQUEST_SCHEMA,\n buildRemediateRequest,\n} from \"./remediate-request.js\";\nexport type { RemediateRequest } from \"./remediate-request.js\";\nexport { checkPatchPolicy } from \"./patch-policy.js\";\nexport type { PolicyContext, PolicyDecision } from \"./patch-policy.js\";\nexport { withWorktree } from \"./worktree.js\";\nexport { codemodRegistry, codemodFor } from \"./codemods/registry.js\";\nexport type { Codemod } from \"./codemods/registry.js\";\nexport { configToggleCodemod } from \"./codemods/config-toggle.js\";\nexport { remediateFindings } from \"./remediate-pipeline.js\";\nexport type {\n RemediateOptions,\n RemediationResult,\n VerifiedPatch,\n RejectedPatch,\n} from \"./remediate-pipeline.js\";\n\n// Scan cancellation / work-budget errors.\nexport { AbortError, BudgetExceededError } from \"./errors.js\";\n\n// Parallel scanning (worker_threads pool). The chunk/merge helpers and their\n// SizedFile/ChunkResult types are internal plumbing (not part of the public API).\nexport { scanParallel } from \"./parallel.js\";\nexport type { ScanChunk } from \"./parallel.js\";\n\n// Detector registry (plugin point). `detectorScope` and the rule-catalog type are\n// internal.\nexport { DetectorRegistry, defaultRegistry } from \"./registry.js\";\n\n// Canonical baseline (shared by qScan + the Action).\nexport {\n fingerprintFinding,\n baselineFromFindings,\n applyBaseline,\n loadBaseline,\n saveBaseline,\n BASELINE_VERSION,\n} from \"./baseline.js\";\nexport type { Baseline } from \"./baseline.js\";\n\n// Incremental scanning: changed-files helper (git-aware, tolerant).\nexport { changedFiles } from \"./changed.js\";\n\n// Optional `quantakrypto.config.json` loader (P2-9; see docs/CONFIG.md).\nexport { loadConfig, ConfigError, CONFIG_FILENAME } from \"./config.js\";\nexport type { QuantakryptoFileConfig, LoadConfigResult } from \"./config.js\";\n\n// HNDL (harvest-now-decrypt-later) data-risk quantifier (see docs/HNDL.md).\nexport {\n HNDL_MODEL_VERSION,\n HNDL_FILENAME,\n SEVERITY_VULNERABILITY,\n CONFIDENCE_WEIGHT,\n CLASSIFICATION_SENSITIVITY,\n NON_HNDL_DISCOUNT,\n DEFAULT_QUANTUM_THREAT_YEARS,\n DEFAULT_MIGRATION_HORIZON_YEARS,\n DEFAULT_UNBOUND_CLASSIFICATION,\n HndlError,\n computeHndl,\n vulnerabilityFactor,\n moscaFactor,\n globMatch,\n findingFingerprint,\n findingScope,\n parseHndlMap,\n loadHndlMap,\n scaffoldHndlYaml,\n} from \"./hndl.js\";\nexport type {\n DataClassification,\n HndlScope,\n HndlDataAsset,\n HndlHorizon,\n HndlDefaults,\n HndlMap,\n ExposureRationale,\n FindingExposure,\n AssetExposure,\n HndlSummary,\n HndlReport,\n} from \"./hndl.js\";\n\n// Filesystem walker (relative POSIX paths, default ignores, size/binary filters).\nexport { walkFiles, isBinaryPath, looksMinified } from \"./walk.js\";\n\n// Analyzable-language coverage (which source languages the scanner inspects).\nexport {\n isAnalyzableSource,\n ANALYZABLE_SOURCE_EXTENSIONS,\n ANALYZABLE_LANGUAGES_LABEL,\n} from \"./detect-utils.js\";\n\n// Inventory + readiness score.\nexport { buildInventory } from \"./inventory.js\";\n\n// Vulnerable-dependency database (the manifest scanner is used internally by scan()).\n// `DEP_VULNERABLE_RULE` is the generic catalog entry for dependency findings\n// (which don't come from a Detector, so aren't in the registry's rule catalog).\nexport { vulnerableDependencies, DEP_VULNERABLE_RULE, isManifestFile } from \"./dependencies.js\";\n\n// Severity utilities (ordering, threshold, SARIF level) — shared across tools.\nexport { SEVERITY_ORDER, severityRank, meetsThreshold, sarifLevel } from \"./severity.js\";\n\n// Reporters.\nexport {\n toSarif,\n toJson,\n formatSummary,\n formatTierGuidance,\n formatProfileGuidance,\n} from \"./report.js\";\nexport type { ReportOptions } from \"./report.js\";\n\n// CycloneDX 1.6 cryptographic bill of materials (CBOM) export.\nexport { toCbom } from \"./cbom.js\";\nexport type { CycloneDxBom, CbomComponent } from \"./cbom.js\";\n// Merge multiple CBOMs (code + infra + live endpoints) into one combined BOM.\nexport { mergeCboms } from \"./cbom-merge.js\";\n// OpenVEX 0.2.0 export — quantum-readiness posture as VEX statements.\nexport { toOpenVex } from \"./vex.js\";\nexport type { OpenVexDocument, OpenVexStatement, OpenVexOptions } from \"./vex.js\";\n// Crypto-agility manifest: agent-consumable crypto-posture document.\nexport {\n buildCryptoAgilityManifest,\n validateCryptoAgilityManifest,\n CRYPTO_AGILITY_MANIFEST_VERSION,\n CRYPTO_AGILITY_WELL_KNOWN_PATH,\n} from \"./crypto-agility.js\";\nexport type {\n CryptoAgilityManifest,\n CryptoAgilityManifestOptions,\n CryptoAgilityPosture,\n CryptoAgilityCbomSummary,\n CryptoAgilityFamily,\n CryptoAgilityPolicy,\n ManifestValidation,\n} from \"./crypto-agility.js\";\n// ISO/IEC 27001 A.8.24 evidence-chain readiness report.\nexport { buildReadinessReport, signReadinessReport, verifyReadinessReport } from \"./evidence.js\";\nexport type {\n ReadinessReport,\n EvidenceFinding,\n ReadinessReportOptions,\n EvidenceSigner,\n SignEvidenceOptions,\n VerifyReadinessResult,\n} from \"./evidence.js\";\n\n// Cryptography policy → per-finding verdicts (A.8.24 evidence §4).\nexport { buildPolicyMapping, parseCryptoPolicy } from \"./policy.js\";\nexport type { CryptoPolicy, PolicyVerdict, PolicyMapping, PolicyFindingVerdict } from \"./policy.js\";\n\n// Compliance mandates → dated, clause-named verdicts + gate (policy-as-code).\nexport {\n MANDATES,\n mandateIds,\n getMandate,\n assertKnownMandates,\n evaluateMandates,\n mandateGateFails,\n} from \"./mandates.js\";\nexport type {\n Mandate,\n MandateRule,\n MandateRuleTier,\n MandateStatus,\n MandateFindingVerdict,\n MandateEvaluation,\n MandateGateOptions,\n} from \"./mandates.js\";\n\n// Remediation lookup (family + tier-aware + profile-aware) and stateful-HBS guidance.\nexport {\n remediationFor,\n remediationForTier,\n remediationForProfile,\n TIER_PARAMS,\n STATEFUL_HBS_NOTE,\n PQC_TRANSITION_NOTE,\n statefulHbsApplies,\n} from \"./remediation.js\";\nexport type { SecurityTier } from \"./remediation.js\";\n\n// Post-quantum standards source of truth + review cadence.\nexport { PQC_STANDARDS } from \"./standards.js\";\nexport type { PqcStandards, StandardsCitation, StandardsReviewStatus } from \"./standards.js\";\n\n// Selectable standards regime profiles (NIST / CNSA / BSI / ANSSI / NCSC).\nexport {\n STANDARDS_PROFILES,\n DEFAULT_PROFILE_ID,\n standardsProfileIds,\n getStandardsProfile,\n defaultStandardsProfile,\n} from \"./standards-profiles.js\";\nexport type { StandardsProfile, HybridStance } from \"./standards-profiles.js\";\n\n// CWE identifier constants.\nexport {\n CWE_BROKEN_CRYPTO,\n CWE_WEAK_STRENGTH,\n CWE_CERT_VALIDATION,\n CWE_HARDCODED_KEY,\n CWE_RISKY_PRIMITIVE,\n} from \"./cwe.js\";\n"]} | ||
| {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @quantakrypto/core — public API (LOCKED CONTRACT).\n *\n * The exported NAMES and SIGNATURES below are the stable contract that\n * @quantakrypto/qscan, @quantakrypto/mcp and the GitHub Action depend on — do not change\n * them without updating all consumers. The implementations live in focused\n * modules under src/ and are re-exported here; the public surface is identical\n * to the original stub file.\n */\nexport * from \"./types.js\";\n\n// Tool version, surfaced in reports. Keep in sync with package.json.\nexport { VERSION } from \"./version.js\";\n\n// Minimal SARIF 2.1.0 log shape, defined alongside the reporters.\nexport type { SarifLog } from \"./report.js\";\n\n// Core orchestration + built-in detector set.\nexport { scan, detectors, detectFile, compareFindings } from \"./scan.js\";\n\n// Snippet-level fix verification (shared by MCP verify_fix + remediation).\nexport { verifyFix, languageToExtension } from \"./verify.js\";\nexport type { VerifyResult } from \"./verify.js\";\n\n// Agent-plane shared types + the context redactor (offline; reused by MCP).\nexport type {\n ContextLevel,\n RedactedContext,\n TriageVerdict,\n Patch,\n FixProposal,\n} from \"./agent-types.js\";\nexport { buildContext, renderPreflight } from \"./redact.js\";\nexport { TRIAGE_RUBRIC, TRIAGE_VERDICT_SCHEMA, buildTriageRequest } from \"./triage.js\";\nexport type { TriageRequest } from \"./triage.js\";\nexport {\n REMEDIATE_RUBRIC,\n FIX_REQUEST_SCHEMA,\n buildRemediateRequest,\n} from \"./remediate-request.js\";\nexport type { RemediateRequest } from \"./remediate-request.js\";\nexport { checkPatchPolicy } from \"./patch-policy.js\";\nexport type { PolicyContext, PolicyDecision } from \"./patch-policy.js\";\nexport { withWorktree } from \"./worktree.js\";\nexport { codemodRegistry, codemodFor } from \"./codemods/registry.js\";\nexport type { Codemod } from \"./codemods/registry.js\";\nexport { configToggleCodemod } from \"./codemods/config-toggle.js\";\nexport { remediateFindings } from \"./remediate-pipeline.js\";\nexport type {\n RemediateOptions,\n RemediationResult,\n VerifiedPatch,\n RejectedPatch,\n} from \"./remediate-pipeline.js\";\n\n// Scan cancellation / work-budget errors.\nexport { AbortError, BudgetExceededError } from \"./errors.js\";\n\n// Parallel scanning (worker_threads pool). The chunk/merge helpers and their\n// SizedFile/ChunkResult types are internal plumbing (not part of the public API).\nexport { scanParallel } from \"./parallel.js\";\nexport type { ScanChunk } from \"./parallel.js\";\n\n// Detector registry (plugin point). `detectorScope` and the rule-catalog type are\n// internal.\nexport { DetectorRegistry, defaultRegistry } from \"./registry.js\";\n\n// Canonical baseline (shared by qScan + the Action).\nexport {\n fingerprintFinding,\n baselineFromFindings,\n applyBaseline,\n loadBaseline,\n saveBaseline,\n BASELINE_VERSION,\n} from \"./baseline.js\";\nexport type { Baseline } from \"./baseline.js\";\n\n// Incremental scanning: changed-files helper (git-aware, tolerant).\nexport { changedFiles } from \"./changed.js\";\n\n// Optional `quantakrypto.config.json` loader (P2-9; see docs/CONFIG.md).\nexport { loadConfig, ConfigError, CONFIG_FILENAME } from \"./config.js\";\nexport type { QuantakryptoFileConfig, LoadConfigResult } from \"./config.js\";\n\n// HNDL (harvest-now-decrypt-later) data-risk quantifier (see docs/HNDL.md).\nexport {\n HNDL_MODEL_VERSION,\n HNDL_FILENAME,\n SEVERITY_VULNERABILITY,\n CONFIDENCE_WEIGHT,\n CLASSIFICATION_SENSITIVITY,\n NON_HNDL_DISCOUNT,\n DEFAULT_QUANTUM_THREAT_YEARS,\n DEFAULT_MIGRATION_HORIZON_YEARS,\n DEFAULT_UNBOUND_CLASSIFICATION,\n HndlError,\n computeHndl,\n vulnerabilityFactor,\n moscaFactor,\n globMatch,\n findingFingerprint,\n findingScope,\n parseHndlMap,\n loadHndlMap,\n scaffoldHndlYaml,\n} from \"./hndl.js\";\nexport type {\n DataClassification,\n HndlScope,\n HndlDataAsset,\n HndlHorizon,\n HndlDefaults,\n HndlMap,\n ExposureRationale,\n FindingExposure,\n AssetExposure,\n HndlSummary,\n HndlReport,\n} from \"./hndl.js\";\n\n// Filesystem walker (relative POSIX paths, default ignores, size/binary filters).\nexport { walkFiles, isBinaryPath, looksMinified } from \"./walk.js\";\n\n// Analyzable-language coverage (which source languages the scanner inspects).\nexport {\n isAnalyzableSource,\n ANALYZABLE_SOURCE_EXTENSIONS,\n ANALYZABLE_LANGUAGES_LABEL,\n} from \"./detect-utils.js\";\n\n// Inventory + readiness score.\nexport { buildInventory } from \"./inventory.js\";\n\n// Vulnerable-dependency database (the manifest scanner is used internally by scan()).\n// `DEP_VULNERABLE_RULE` is the generic catalog entry for dependency findings\n// (which don't come from a Detector, so aren't in the registry's rule catalog).\nexport { vulnerableDependencies, DEP_VULNERABLE_RULE, isManifestFile } from \"./dependencies.js\";\n\n// Dependency-advisory scanning (opt-in via `qscan --audit`): shells out to each\n// ecosystem's own audit tool. `DEP_ADVISORY_RULE` is the generic SARIF catalog\n// entry (advisory findings don't come from a Detector).\nexport { scanAdvisories, DEP_ADVISORY_RULE } from \"./advisories.js\";\nexport type { ScanAdvisoriesOptions, ExecFn } from \"./advisories.js\";\n\n// Provenance / declared-source-repository check (opt-in via `qscan --audit`). The\n// network HEAD request is INJECTED by the (networked) caller so core stays\n// offline (ADR-0005). `PROVENANCE_RULES` are its generic SARIF catalog entries.\nexport { checkProvenance, normalizeRepoUrl, PROVENANCE_RULES } from \"./provenance.js\";\nexport type { ProvenanceOptions, RepoHeadRequester, RepoHeadOutcome } from \"./provenance.js\";\n\n// Severity utilities (ordering, threshold, SARIF level) — shared across tools.\nexport { SEVERITY_ORDER, severityRank, meetsThreshold, sarifLevel } from \"./severity.js\";\n\n// Reporters.\nexport {\n toSarif,\n toJson,\n formatSummary,\n formatTierGuidance,\n formatProfileGuidance,\n} from \"./report.js\";\nexport type { ReportOptions } from \"./report.js\";\n\n// CycloneDX 1.6 cryptographic bill of materials (CBOM) export.\nexport { toCbom } from \"./cbom.js\";\nexport type { CycloneDxBom, CbomComponent } from \"./cbom.js\";\n// Merge multiple CBOMs (code + infra + live endpoints) into one combined BOM.\nexport { mergeCboms } from \"./cbom-merge.js\";\n// OpenVEX 0.2.0 export — quantum-readiness posture as VEX statements.\nexport { toOpenVex } from \"./vex.js\";\nexport type { OpenVexDocument, OpenVexStatement, OpenVexOptions } from \"./vex.js\";\n// Crypto-agility manifest: agent-consumable crypto-posture document.\nexport {\n buildCryptoAgilityManifest,\n validateCryptoAgilityManifest,\n CRYPTO_AGILITY_MANIFEST_VERSION,\n CRYPTO_AGILITY_WELL_KNOWN_PATH,\n} from \"./crypto-agility.js\";\nexport type {\n CryptoAgilityManifest,\n CryptoAgilityManifestOptions,\n CryptoAgilityPosture,\n CryptoAgilityCbomSummary,\n CryptoAgilityFamily,\n CryptoAgilityPolicy,\n ManifestValidation,\n} from \"./crypto-agility.js\";\n// ISO/IEC 27001 A.8.24 evidence-chain readiness report.\nexport { buildReadinessReport, signReadinessReport, verifyReadinessReport } from \"./evidence.js\";\nexport type {\n ReadinessReport,\n EvidenceFinding,\n ReadinessReportOptions,\n EvidenceSigner,\n SignEvidenceOptions,\n VerifyReadinessResult,\n} from \"./evidence.js\";\n\n// Cryptography policy → per-finding verdicts (A.8.24 evidence §4).\nexport { buildPolicyMapping, parseCryptoPolicy } from \"./policy.js\";\nexport type { CryptoPolicy, PolicyVerdict, PolicyMapping, PolicyFindingVerdict } from \"./policy.js\";\n\n// Compliance mandates → dated, clause-named verdicts + gate (policy-as-code).\nexport {\n MANDATES,\n mandateIds,\n getMandate,\n assertKnownMandates,\n evaluateMandates,\n mandateGateFails,\n} from \"./mandates.js\";\nexport type {\n Mandate,\n MandateRule,\n MandateRuleTier,\n MandateStatus,\n MandateFindingVerdict,\n MandateEvaluation,\n MandateGateOptions,\n} from \"./mandates.js\";\n\n// Remediation lookup (family + tier-aware + profile-aware) and stateful-HBS guidance.\nexport {\n remediationFor,\n remediationForTier,\n remediationForProfile,\n TIER_PARAMS,\n STATEFUL_HBS_NOTE,\n PQC_TRANSITION_NOTE,\n statefulHbsApplies,\n} from \"./remediation.js\";\nexport type { SecurityTier } from \"./remediation.js\";\n\n// Post-quantum standards source of truth + review cadence.\nexport { PQC_STANDARDS } from \"./standards.js\";\nexport type { PqcStandards, StandardsCitation, StandardsReviewStatus } from \"./standards.js\";\n\n// Selectable standards regime profiles (NIST / CNSA / BSI / ANSSI / NCSC).\nexport {\n STANDARDS_PROFILES,\n DEFAULT_PROFILE_ID,\n standardsProfileIds,\n getStandardsProfile,\n defaultStandardsProfile,\n} from \"./standards-profiles.js\";\nexport type { StandardsProfile, HybridStance } from \"./standards-profiles.js\";\n\n// CWE identifier constants.\nexport {\n CWE_BROKEN_CRYPTO,\n CWE_WEAK_STRENGTH,\n CWE_CERT_VALIDATION,\n CWE_HARDCODED_KEY,\n CWE_RISKY_PRIMITIVE,\n} from \"./cwe.js\";\n"]} |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAgDpE,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,CAAC,EAAE,QAAQ,GAAG,aAAa,CAExD;AAED,+FAA+F;AAC/F,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED;;;GAGG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA+B;IACpD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgB;IAEtC,4EAA4E;gBAChE,OAAO,GAAE,SAAS,QAAQ,EAAO;IAI7C,kFAAkF;IAClF,QAAQ,CAAC,CAAC,EAAE,QAAQ,GAAG,IAAI;IAS3B,wDAAwD;IACxD,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS;IAIrC,qDAAqD;IACrD,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAIxB,uDAAuD;IACvD,GAAG,IAAI,QAAQ,EAAE;IAIjB;;;;;;OAMG;IACH,WAAW,IAAI,QAAQ,EAAE;IAezB,gFAAgF;IAChF,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS;IASrD,uEAAuE;IACvE,KAAK,IAAI,gBAAgB;CAG1B;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,EAAE,QAAQ,EA+CtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,kBAAyC,CAAC"} | ||
| {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAiDpE,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,CAAC,EAAE,QAAQ,GAAG,aAAa,CAExD;AAED,+FAA+F;AAC/F,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED;;;GAGG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA+B;IACpD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgB;IAEtC,4EAA4E;gBAChE,OAAO,GAAE,SAAS,QAAQ,EAAO;IAI7C,kFAAkF;IAClF,QAAQ,CAAC,CAAC,EAAE,QAAQ,GAAG,IAAI;IAS3B,wDAAwD;IACxD,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS;IAIrC,qDAAqD;IACrD,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAIxB,uDAAuD;IACvD,GAAG,IAAI,QAAQ,EAAE;IAIjB;;;;;;OAMG;IACH,WAAW,IAAI,QAAQ,EAAE;IAezB,gFAAgF;IAChF,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS;IASrD,uEAAuE;IACvE,KAAK,IAAI,gBAAgB;CAG1B;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,EAAE,QAAQ,EAgDtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,kBAAyC,CAAC"} |
+2
-0
@@ -34,2 +34,3 @@ import { sourceDetectors } from "./detectors/source.js"; | ||
| import { weakHashDetector } from "./detectors/weak-hash.js"; | ||
| import { pqcParameterDetector } from "./detectors/pqc-parameter.js"; | ||
| import { cloudformationDetector } from "./detectors/cloudformation.js"; | ||
@@ -170,2 +171,3 @@ import { bicepDetector } from "./detectors/bicep.js"; | ||
| weakHashDetector, | ||
| pqcParameterDetector, | ||
| cloudformationDetector, | ||
@@ -172,0 +174,0 @@ bicepDetector, |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Detector registry — the plugin point for source/config detectors.\n *\n * Instead of `scan()` closing over a hardcoded array and inferring scope from\n * ruleId prefixes, detectors are registered with a declared `scope` and\n * `language` (see {@link Detector}). `scan()` consults a registry (the\n * {@link defaultRegistry} by default, or an explicit `detectors` override) and\n * honours the source/config toggles by each detector's declared scope.\n *\n * To add a language or detector, see the \"Adding a detector / language\" section\n * of the package README.\n */\nimport type { Detector, DetectorScope, RuleMeta } from \"./types.js\";\nimport { sourceDetectors } from \"./detectors/source.js\";\nimport { pythonDetector } from \"./detectors/python.js\";\nimport { goDetector } from \"./detectors/go.js\";\nimport { javaDetector } from \"./detectors/java.js\";\nimport { csharpDetector } from \"./detectors/csharp.js\";\nimport { rustDetector } from \"./detectors/rust.js\";\nimport { rubyDetector } from \"./detectors/ruby.js\";\nimport { phpDetector } from \"./detectors/php.js\";\nimport { elixirDetector } from \"./detectors/elixir.js\";\nimport { cDetector } from \"./detectors/c.js\";\nimport { swiftDetector } from \"./detectors/swift.js\";\nimport { objcDetector } from \"./detectors/objc.js\";\nimport { dartDetector } from \"./detectors/dart.js\";\nimport { solidityDetector } from \"./detectors/solidity.js\";\nimport { pemDetector } from \"./detectors/pem.js\";\nimport { jwkDetector } from \"./detectors/jwk.js\";\nimport { terraformDetector } from \"./detectors/terraform.js\";\nimport { cloudKmsDetector } from \"./detectors/cloud-kms.js\";\nimport { cicdDetector } from \"./detectors/cicd.js\";\nimport { secretsDetector } from \"./detectors/secrets.js\";\nimport { joseDetector } from \"./detectors/jose.js\";\nimport { k8sDetector } from \"./detectors/k8s.js\";\nimport { messagingDetector } from \"./detectors/messaging.js\";\nimport { databaseDetector } from \"./detectors/database.js\";\nimport { xmldsigDetector } from \"./detectors/xmldsig.js\";\nimport { pkcs11Detector } from \"./detectors/pkcs11.js\";\nimport { dkimDetector } from \"./detectors/dkim.js\";\nimport { sshCaDetector } from \"./detectors/ssh-ca.js\";\nimport { spireDetector } from \"./detectors/spire.js\";\nimport { proxyDetector } from \"./detectors/proxy.js\";\nimport { webauthnDetector } from \"./detectors/webauthn.js\";\nimport { codesignDetector } from \"./detectors/codesign.js\";\nimport { weakHashDetector } from \"./detectors/weak-hash.js\";\nimport { cloudformationDetector } from \"./detectors/cloudformation.js\";\nimport { bicepDetector } from \"./detectors/bicep.js\";\nimport { pulumiDetector } from \"./detectors/pulumi.js\";\nimport { meshDetector } from \"./detectors/mesh.js\";\nimport { dnssecDetector } from \"./detectors/dnssec.js\";\nimport { vpnDetector } from \"./detectors/vpn.js\";\nimport { ansibleDetector } from \"./detectors/ansible.js\";\nimport { ageDetector } from \"./detectors/age.js\";\nimport { supplyChainDetector } from \"./detectors/supply-chain.js\";\nimport { vaultDetector } from \"./detectors/vault.js\";\nimport { keystoreDetector } from \"./detectors/keystore.js\";\nimport { openpgpDetector } from \"./detectors/openpgp.js\";\nimport { statefulHbsDetector } from \"./detectors/stateful-hbs.js\";\n\n/** Normalised scope of a detector (defaults to \"source\" when undeclared). */\nexport function detectorScope(d: Detector): DetectorScope {\n return d.scope ?? \"source\";\n}\n\n/** A rule plus the detector that emits it — the result of {@link DetectorRegistry.forRule}. */\nexport interface RuleCatalogEntry {\n rule: RuleMeta;\n detector: Detector;\n}\n\n/**\n * An ordered, id-indexed collection of detectors. Registration order is\n * preserved by {@link all} for deterministic scan output. Ids must be unique.\n */\nexport class DetectorRegistry {\n private readonly byId = new Map<string, Detector>();\n private readonly order: string[] = [];\n\n /** Construct a registry, optionally seeded with an initial detector set. */\n constructor(initial: readonly Detector[] = []) {\n for (const d of initial) this.register(d);\n }\n\n /** Register a detector. Throws on a duplicate id. Returns `this` for chaining. */\n register(d: Detector): this {\n if (this.byId.has(d.id)) {\n throw new Error(`duplicate detector id: ${d.id}`);\n }\n this.byId.set(d.id, d);\n this.order.push(d.id);\n return this;\n }\n\n /** Look up a detector by its id (exact, not prefix). */\n get(id: string): Detector | undefined {\n return this.byId.get(id);\n }\n\n /** True if a detector with this id is registered. */\n has(id: string): boolean {\n return this.byId.has(id);\n }\n\n /** All registered detectors, in registration order. */\n all(): Detector[] {\n return this.order.map((id) => this.byId.get(id)!);\n }\n\n /**\n * The flattened rule catalog: every {@link RuleMeta} declared by every\n * registered detector, in detector-registration then in-detector order. This\n * is the single source of truth for rule metadata consumed by SARIF\n * `rules[]`, the MCP `explain_finding` resolver, and per-rule enable/disable.\n * Duplicate rule ids across detectors throw (ids are globally unique).\n */\n ruleCatalog(): RuleMeta[] {\n const out: RuleMeta[] = [];\n const seen = new Set<string>();\n for (const det of this.all()) {\n for (const rule of det.rules ?? []) {\n if (seen.has(rule.id)) {\n throw new Error(`duplicate rule id in catalog: ${rule.id}`);\n }\n seen.add(rule.id);\n out.push(rule);\n }\n }\n return out;\n }\n\n /** Resolve a rule id to its {@link RuleMeta} and the detector that emits it. */\n forRule(ruleId: string): RuleCatalogEntry | undefined {\n for (const det of this.all()) {\n for (const rule of det.rules ?? []) {\n if (rule.id === ruleId) return { rule, detector: det };\n }\n }\n return undefined;\n }\n\n /** A shallow copy of this registry (useful to extend the defaults). */\n clone(): DetectorRegistry {\n return new DetectorRegistry(this.all());\n }\n}\n\n/**\n * The built-in detectors, in run order: the JS/TS source + language-agnostic config\n * detectors, the per-language source packs (Python, Go, Java/Kotlin/Scala, C#, Rust,\n * Ruby, PHP, Elixir, C/C++, Swift, Objective-C, Dart, Solidity/Move/Cairo), the\n * infrastructure/config detectors (Terraform, Bicep, Pulumi, CloudFormation, cloud-KMS,\n * k8s, mesh, DNSSEC, Vault, database/TDE, messaging, VPN, Ansible, supply-chain, CI/CD,\n * secrets, age, keystore, OpenPGP, JWK, XML-DSig/SAML, PKCS#11/HSM, DKIM, SSH-CA,\n * SPIFFE/SPIRE, reverse-proxy/gRPC TLS, WebAuthn/FIDO2, code-signing, weak-hash-in-\n * signature), and the stateful-HBS (SP 800-208) detector. The manifest (dependency)\n * scanner is handled separately by `scan()`.\n *\n * This is the single source of truth for the default detector set: both\n * {@link defaultRegistry} and the public `detectors` export (re-exported from\n * `scan.ts`) are built from it, so the two can never drift out of sync.\n */\nexport const builtinDetectors: Detector[] = [\n ...sourceDetectors,\n pythonDetector,\n goDetector,\n javaDetector,\n csharpDetector,\n rustDetector,\n rubyDetector,\n phpDetector,\n elixirDetector,\n cDetector,\n swiftDetector,\n objcDetector,\n dartDetector,\n solidityDetector,\n pemDetector,\n jwkDetector,\n terraformDetector,\n cloudKmsDetector,\n cicdDetector,\n secretsDetector,\n joseDetector,\n k8sDetector,\n messagingDetector,\n databaseDetector,\n xmldsigDetector,\n pkcs11Detector,\n dkimDetector,\n sshCaDetector,\n spireDetector,\n proxyDetector,\n webauthnDetector,\n codesignDetector,\n weakHashDetector,\n cloudformationDetector,\n bicepDetector,\n pulumiDetector,\n meshDetector,\n dnssecDetector,\n vpnDetector,\n ansibleDetector,\n ageDetector,\n supplyChainDetector,\n vaultDetector,\n keystoreDetector,\n openpgpDetector,\n statefulHbsDetector,\n];\n\n/**\n * The default registry, preloaded with {@link builtinDetectors}. Used by\n * `scan()` whenever `options.detectors` is not supplied.\n */\nexport const defaultRegistry = new DetectorRegistry(builtinDetectors);\n"]} | ||
| {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACpE,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAElE,6EAA6E;AAC7E,MAAM,UAAU,aAAa,CAAC,CAAW;IACvC,OAAO,CAAC,CAAC,KAAK,IAAI,QAAQ,CAAC;AAC7B,CAAC;AAQD;;;GAGG;AACH,MAAM,OAAO,gBAAgB;IACV,IAAI,GAAG,IAAI,GAAG,EAAoB,CAAC;IACnC,KAAK,GAAa,EAAE,CAAC;IAEtC,4EAA4E;IAC5E,YAAY,UAA+B,EAAE;QAC3C,KAAK,MAAM,CAAC,IAAI,OAAO;YAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC5C,CAAC;IAED,kFAAkF;IAClF,QAAQ,CAAC,CAAW;QAClB,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACpD,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;QACvB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACtB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,wDAAwD;IACxD,GAAG,CAAC,EAAU;QACZ,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC3B,CAAC;IAED,qDAAqD;IACrD,GAAG,CAAC,EAAU;QACZ,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC3B,CAAC;IAED,uDAAuD;IACvD,GAAG;QACD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAE,CAAC,CAAC;IACpD,CAAC;IAED;;;;;;OAMG;IACH,WAAW;QACT,MAAM,GAAG,GAAe,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;QAC/B,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;YAC7B,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC;gBACnC,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;oBACtB,MAAM,IAAI,KAAK,CAAC,iCAAiC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;gBAC9D,CAAC;gBACD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;gBAClB,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACjB,CAAC;QACH,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAED,gFAAgF;IAChF,OAAO,CAAC,MAAc;QACpB,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;YAC7B,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC;gBACnC,IAAI,IAAI,CAAC,EAAE,KAAK,MAAM;oBAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC;YACzD,CAAC;QACH,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,uEAAuE;IACvE,KAAK;QACH,OAAO,IAAI,gBAAgB,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAC1C,CAAC;CACF;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAe;IAC1C,GAAG,eAAe;IAClB,cAAc;IACd,UAAU;IACV,YAAY;IACZ,cAAc;IACd,YAAY;IACZ,YAAY;IACZ,WAAW;IACX,cAAc;IACd,SAAS;IACT,aAAa;IACb,YAAY;IACZ,YAAY;IACZ,gBAAgB;IAChB,WAAW;IACX,WAAW;IACX,iBAAiB;IACjB,gBAAgB;IAChB,YAAY;IACZ,eAAe;IACf,YAAY;IACZ,WAAW;IACX,iBAAiB;IACjB,gBAAgB;IAChB,eAAe;IACf,cAAc;IACd,YAAY;IACZ,aAAa;IACb,aAAa;IACb,aAAa;IACb,gBAAgB;IAChB,gBAAgB;IAChB,gBAAgB;IAChB,oBAAoB;IACpB,sBAAsB;IACtB,aAAa;IACb,cAAc;IACd,YAAY;IACZ,cAAc;IACd,WAAW;IACX,eAAe;IACf,WAAW;IACX,mBAAmB;IACnB,aAAa;IACb,gBAAgB;IAChB,eAAe;IACf,mBAAmB;CACpB,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,gBAAgB,CAAC,gBAAgB,CAAC,CAAC","sourcesContent":["/**\n * Detector registry — the plugin point for source/config detectors.\n *\n * Instead of `scan()` closing over a hardcoded array and inferring scope from\n * ruleId prefixes, detectors are registered with a declared `scope` and\n * `language` (see {@link Detector}). `scan()` consults a registry (the\n * {@link defaultRegistry} by default, or an explicit `detectors` override) and\n * honours the source/config toggles by each detector's declared scope.\n *\n * To add a language or detector, see the \"Adding a detector / language\" section\n * of the package README.\n */\nimport type { Detector, DetectorScope, RuleMeta } from \"./types.js\";\nimport { sourceDetectors } from \"./detectors/source.js\";\nimport { pythonDetector } from \"./detectors/python.js\";\nimport { goDetector } from \"./detectors/go.js\";\nimport { javaDetector } from \"./detectors/java.js\";\nimport { csharpDetector } from \"./detectors/csharp.js\";\nimport { rustDetector } from \"./detectors/rust.js\";\nimport { rubyDetector } from \"./detectors/ruby.js\";\nimport { phpDetector } from \"./detectors/php.js\";\nimport { elixirDetector } from \"./detectors/elixir.js\";\nimport { cDetector } from \"./detectors/c.js\";\nimport { swiftDetector } from \"./detectors/swift.js\";\nimport { objcDetector } from \"./detectors/objc.js\";\nimport { dartDetector } from \"./detectors/dart.js\";\nimport { solidityDetector } from \"./detectors/solidity.js\";\nimport { pemDetector } from \"./detectors/pem.js\";\nimport { jwkDetector } from \"./detectors/jwk.js\";\nimport { terraformDetector } from \"./detectors/terraform.js\";\nimport { cloudKmsDetector } from \"./detectors/cloud-kms.js\";\nimport { cicdDetector } from \"./detectors/cicd.js\";\nimport { secretsDetector } from \"./detectors/secrets.js\";\nimport { joseDetector } from \"./detectors/jose.js\";\nimport { k8sDetector } from \"./detectors/k8s.js\";\nimport { messagingDetector } from \"./detectors/messaging.js\";\nimport { databaseDetector } from \"./detectors/database.js\";\nimport { xmldsigDetector } from \"./detectors/xmldsig.js\";\nimport { pkcs11Detector } from \"./detectors/pkcs11.js\";\nimport { dkimDetector } from \"./detectors/dkim.js\";\nimport { sshCaDetector } from \"./detectors/ssh-ca.js\";\nimport { spireDetector } from \"./detectors/spire.js\";\nimport { proxyDetector } from \"./detectors/proxy.js\";\nimport { webauthnDetector } from \"./detectors/webauthn.js\";\nimport { codesignDetector } from \"./detectors/codesign.js\";\nimport { weakHashDetector } from \"./detectors/weak-hash.js\";\nimport { pqcParameterDetector } from \"./detectors/pqc-parameter.js\";\nimport { cloudformationDetector } from \"./detectors/cloudformation.js\";\nimport { bicepDetector } from \"./detectors/bicep.js\";\nimport { pulumiDetector } from \"./detectors/pulumi.js\";\nimport { meshDetector } from \"./detectors/mesh.js\";\nimport { dnssecDetector } from \"./detectors/dnssec.js\";\nimport { vpnDetector } from \"./detectors/vpn.js\";\nimport { ansibleDetector } from \"./detectors/ansible.js\";\nimport { ageDetector } from \"./detectors/age.js\";\nimport { supplyChainDetector } from \"./detectors/supply-chain.js\";\nimport { vaultDetector } from \"./detectors/vault.js\";\nimport { keystoreDetector } from \"./detectors/keystore.js\";\nimport { openpgpDetector } from \"./detectors/openpgp.js\";\nimport { statefulHbsDetector } from \"./detectors/stateful-hbs.js\";\n\n/** Normalised scope of a detector (defaults to \"source\" when undeclared). */\nexport function detectorScope(d: Detector): DetectorScope {\n return d.scope ?? \"source\";\n}\n\n/** A rule plus the detector that emits it — the result of {@link DetectorRegistry.forRule}. */\nexport interface RuleCatalogEntry {\n rule: RuleMeta;\n detector: Detector;\n}\n\n/**\n * An ordered, id-indexed collection of detectors. Registration order is\n * preserved by {@link all} for deterministic scan output. Ids must be unique.\n */\nexport class DetectorRegistry {\n private readonly byId = new Map<string, Detector>();\n private readonly order: string[] = [];\n\n /** Construct a registry, optionally seeded with an initial detector set. */\n constructor(initial: readonly Detector[] = []) {\n for (const d of initial) this.register(d);\n }\n\n /** Register a detector. Throws on a duplicate id. Returns `this` for chaining. */\n register(d: Detector): this {\n if (this.byId.has(d.id)) {\n throw new Error(`duplicate detector id: ${d.id}`);\n }\n this.byId.set(d.id, d);\n this.order.push(d.id);\n return this;\n }\n\n /** Look up a detector by its id (exact, not prefix). */\n get(id: string): Detector | undefined {\n return this.byId.get(id);\n }\n\n /** True if a detector with this id is registered. */\n has(id: string): boolean {\n return this.byId.has(id);\n }\n\n /** All registered detectors, in registration order. */\n all(): Detector[] {\n return this.order.map((id) => this.byId.get(id)!);\n }\n\n /**\n * The flattened rule catalog: every {@link RuleMeta} declared by every\n * registered detector, in detector-registration then in-detector order. This\n * is the single source of truth for rule metadata consumed by SARIF\n * `rules[]`, the MCP `explain_finding` resolver, and per-rule enable/disable.\n * Duplicate rule ids across detectors throw (ids are globally unique).\n */\n ruleCatalog(): RuleMeta[] {\n const out: RuleMeta[] = [];\n const seen = new Set<string>();\n for (const det of this.all()) {\n for (const rule of det.rules ?? []) {\n if (seen.has(rule.id)) {\n throw new Error(`duplicate rule id in catalog: ${rule.id}`);\n }\n seen.add(rule.id);\n out.push(rule);\n }\n }\n return out;\n }\n\n /** Resolve a rule id to its {@link RuleMeta} and the detector that emits it. */\n forRule(ruleId: string): RuleCatalogEntry | undefined {\n for (const det of this.all()) {\n for (const rule of det.rules ?? []) {\n if (rule.id === ruleId) return { rule, detector: det };\n }\n }\n return undefined;\n }\n\n /** A shallow copy of this registry (useful to extend the defaults). */\n clone(): DetectorRegistry {\n return new DetectorRegistry(this.all());\n }\n}\n\n/**\n * The built-in detectors, in run order: the JS/TS source + language-agnostic config\n * detectors, the per-language source packs (Python, Go, Java/Kotlin/Scala, C#, Rust,\n * Ruby, PHP, Elixir, C/C++, Swift, Objective-C, Dart, Solidity/Move/Cairo), the\n * infrastructure/config detectors (Terraform, Bicep, Pulumi, CloudFormation, cloud-KMS,\n * k8s, mesh, DNSSEC, Vault, database/TDE, messaging, VPN, Ansible, supply-chain, CI/CD,\n * secrets, age, keystore, OpenPGP, JWK, XML-DSig/SAML, PKCS#11/HSM, DKIM, SSH-CA,\n * SPIFFE/SPIRE, reverse-proxy/gRPC TLS, WebAuthn/FIDO2, code-signing, weak-hash-in-\n * signature), and the stateful-HBS (SP 800-208) detector. The manifest (dependency)\n * scanner is handled separately by `scan()`.\n *\n * This is the single source of truth for the default detector set: both\n * {@link defaultRegistry} and the public `detectors` export (re-exported from\n * `scan.ts`) are built from it, so the two can never drift out of sync.\n */\nexport const builtinDetectors: Detector[] = [\n ...sourceDetectors,\n pythonDetector,\n goDetector,\n javaDetector,\n csharpDetector,\n rustDetector,\n rubyDetector,\n phpDetector,\n elixirDetector,\n cDetector,\n swiftDetector,\n objcDetector,\n dartDetector,\n solidityDetector,\n pemDetector,\n jwkDetector,\n terraformDetector,\n cloudKmsDetector,\n cicdDetector,\n secretsDetector,\n joseDetector,\n k8sDetector,\n messagingDetector,\n databaseDetector,\n xmldsigDetector,\n pkcs11Detector,\n dkimDetector,\n sshCaDetector,\n spireDetector,\n proxyDetector,\n webauthnDetector,\n codesignDetector,\n weakHashDetector,\n pqcParameterDetector,\n cloudformationDetector,\n bicepDetector,\n pulumiDetector,\n meshDetector,\n dnssecDetector,\n vpnDetector,\n ansibleDetector,\n ageDetector,\n supplyChainDetector,\n vaultDetector,\n keystoreDetector,\n openpgpDetector,\n statefulHbsDetector,\n];\n\n/**\n * The default registry, preloaded with {@link builtinDetectors}. Used by\n * `scan()` whenever `options.detectors` is not supplied.\n */\nexport const defaultRegistry = new DetectorRegistry(builtinDetectors);\n"]} |
@@ -6,3 +6,3 @@ /** | ||
| */ | ||
| export declare const VERSION = "0.7.0"; | ||
| export declare const VERSION = "0.8.0"; | ||
| //# sourceMappingURL=version.d.ts.map |
+1
-1
@@ -6,3 +6,3 @@ /** | ||
| */ | ||
| export const VERSION = "0.7.0"; | ||
| export const VERSION = "0.8.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.7.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.8.0\";\n"]} |
+1
-1
| { | ||
| "name": "@quantakrypto/core", | ||
| "version": "0.7.0", | ||
| "version": "0.8.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", |
+22
-0
@@ -140,2 +140,4 @@ # @quantakrypto/core | ||
| | `ssh-cert` | config | SSH public keys (`ssh-rsa`, `ssh-ed25519`, `ecdsa-sha2-*`) and X.509 certificate signature algorithms (`sha256WithRSAEncryption`, `ecdsa-with-SHA256`, …) | | ||
| | `weak-hash-signature` | config | SHA-1/MD5 in a digital-signature or X.509 certificate algorithm (`SHA1withRSA`, `sha1WithRSAEncryption` + OID, `openssl -sha1` in a cert/sign command) | | ||
| | `pqc-parameter` | config | Post-quantum KEM parameter checks: pre-standard round-3 Kyber claimed as FIPS 203 ML-KEM (`pqc-prestandard-kem`), and an ML-KEM/Kyber byte size that names a different parameter set than the code advertises (`pqc-parameter-mismatch`) | | ||
@@ -225,2 +227,22 @@ The rows above cover JavaScript/TypeScript plus the language-agnostic PEM/SSH/TLS | ||
| ### `scanAdvisories(root, opts?)` / `checkProvenance(root, opts?)` (opt-in, `qscan --audit`) | ||
| Two supply-chain helpers layered on top of the crypto scan (opt-in because they | ||
| shell out or make a network request): | ||
| - **`scanAdvisories(root, opts?): Promise<{ findings; diagnostics }>`** — shells | ||
| out (`execFile`, bounded timeout + buffer, in a try/catch — the blessed | ||
| `changed.ts` pattern) to each present ecosystem's own audit tool | ||
| (`cargo audit --json`, `pip-audit --format json`, `npm audit --json`) and turns | ||
| its advisories into `dep-advisory` (`category: "dependency"`) findings. A | ||
| missing tool (`ENOENT`) or any error degrades to a diagnostic string, never | ||
| throws. `DEP_ADVISORY_RULE` is the generic SARIF catalog entry. | ||
| - **`checkProvenance(root, opts?): Promise<{ findings; diagnostics }>`** — reads | ||
| the root manifest's declared repository (`package.json` / `Cargo.toml` / | ||
| `pyproject.toml`). No repository → `provenance-repo-missing` (info). With | ||
| `opts.network` **and** an injected `head` requester, a declared URL that 404s or | ||
| does not resolve → `provenance-repo-unresolved` (medium). Core stays offline | ||
| (ADR-0005): the actual `node:https` HEAD request is **injected** by the caller | ||
| (qScan). `PROVENANCE_RULES` are the generic SARIF catalog entries. | ||
| ### `buildInventory(findings: Finding[]): CryptoInventory` | ||
@@ -227,0 +249,0 @@ |
+12
-0
@@ -140,2 +140,14 @@ /** | ||
| // Dependency-advisory scanning (opt-in via `qscan --audit`): shells out to each | ||
| // ecosystem's own audit tool. `DEP_ADVISORY_RULE` is the generic SARIF catalog | ||
| // entry (advisory findings don't come from a Detector). | ||
| export { scanAdvisories, DEP_ADVISORY_RULE } from "./advisories.js"; | ||
| export type { ScanAdvisoriesOptions, ExecFn } from "./advisories.js"; | ||
| // Provenance / declared-source-repository check (opt-in via `qscan --audit`). The | ||
| // network HEAD request is INJECTED by the (networked) caller so core stays | ||
| // offline (ADR-0005). `PROVENANCE_RULES` are its generic SARIF catalog entries. | ||
| export { checkProvenance, normalizeRepoUrl, PROVENANCE_RULES } from "./provenance.js"; | ||
| export type { ProvenanceOptions, RepoHeadRequester, RepoHeadOutcome } from "./provenance.js"; | ||
| // Severity utilities (ordering, threshold, SARIF level) — shared across tools. | ||
@@ -142,0 +154,0 @@ export { SEVERITY_ORDER, severityRank, meetsThreshold, sarifLevel } from "./severity.js"; |
+2
-0
@@ -47,2 +47,3 @@ /** | ||
| import { weakHashDetector } from "./detectors/weak-hash.js"; | ||
| import { pqcParameterDetector } from "./detectors/pqc-parameter.js"; | ||
| import { cloudformationDetector } from "./detectors/cloudformation.js"; | ||
@@ -198,2 +199,3 @@ import { bicepDetector } from "./detectors/bicep.js"; | ||
| weakHashDetector, | ||
| pqcParameterDetector, | ||
| cloudformationDetector, | ||
@@ -200,0 +202,0 @@ bicepDetector, |
+1
-1
@@ -6,2 +6,2 @@ /** | ||
| */ | ||
| export const VERSION = "0.7.0"; | ||
| export const VERSION = "0.8.0"; |
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
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.
3110253
4.36%453
3.42%39135
4.22%443
5.23%25
19.05%6
50%