🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@quantakrypto/core

Package Overview
Dependencies
Maintainers
1
Versions
17
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@quantakrypto/core - npm Package Compare versions

Comparing version
0.7.0
to
0.8.0
+41
dist/advisories.d.ts
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

@@ -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":"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"}

@@ -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"}

@@ -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":"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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 { 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

@@ -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"]}
{
"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",

@@ -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 @@

@@ -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";

@@ -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,

@@ -6,2 +6,2 @@ /**

*/
export const VERSION = "0.7.0";
export const VERSION = "0.8.0";