@clear-capabilities/agentic-security-scanner 0.148.0 → 0.148.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +150 -0
- package/bin/agentic-security.js +5 -1
- package/dist/4970.index.js +58 -2
- package/dist/agentic-security.mjs +2 -2
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/compliance-frameworks/ccpa.json +2 -0
- package/dist/compliance-frameworks/eu-ai-act.json +12 -16
- package/dist/compliance-frameworks/gdpr.json +8 -6
- package/dist/compliance-frameworks/hipaa-security-rule.json +9 -9
- package/dist/compliance-frameworks/nist-800-171-r3.json +6 -5
- package/dist/compliance-frameworks/nist-ai-600-1.json +6 -4
- package/dist/compliance-frameworks/nist-csf-2.json +6 -5
- package/dist/compliance-frameworks/nist-privacy-1-1.json +20 -10
- package/dist/compliance-frameworks/owasp-asvs-5.json +2 -0
- package/dist/compliance-frameworks/owasp-llm-top-10.json +10 -11
- package/package.json +5 -4
- package/src/engine.js +51 -4
- package/src/pipeline/assurance-mode.js +58 -2
- package/src/posture/accuracy-scorecard.js +34 -0
- package/src/posture/aibom.js +22 -0
- package/src/posture/artifact-registry.js +2 -0
- package/src/posture/auditor-walkthrough.js +91 -35
- package/src/posture/compliance-frameworks/ccpa.json +2 -0
- package/src/posture/compliance-frameworks/eu-ai-act.json +12 -16
- package/src/posture/compliance-frameworks/gdpr.json +8 -6
- package/src/posture/compliance-frameworks/hipaa-security-rule.json +9 -9
- package/src/posture/compliance-frameworks/nist-800-171-r3.json +6 -5
- package/src/posture/compliance-frameworks/nist-ai-600-1.json +6 -4
- package/src/posture/compliance-frameworks/nist-csf-2.json +6 -5
- package/src/posture/compliance-frameworks/nist-privacy-1-1.json +20 -10
- package/src/posture/compliance-frameworks/owasp-asvs-5.json +2 -0
- package/src/posture/compliance-frameworks/owasp-llm-top-10.json +10 -11
- package/src/posture/threat-model.js +20 -3
- package/src/posture/verifier.js +70 -0
- package/src/sast/python-sinks.js +24 -1
package/src/engine.js
CHANGED
|
@@ -257,6 +257,7 @@ import { generateBundles as generateExploitBundles } from './posture/exploit-bun
|
|
|
257
257
|
import { buildMigrationPlan as buildPqcPlan, persistMigrationPlan as persistPqcPlan } from './posture/pqc-migration-plan.js';
|
|
258
258
|
import { analyzeLicenseGraph, loadLicenseGraphPolicy } from './posture/license-graph.js';
|
|
259
259
|
import { generateAttributions, persistAttributions } from './posture/license-attributions.js';
|
|
260
|
+
import { buildAIBOM, persistAIBOM } from './posture/aibom.js';
|
|
260
261
|
import { annotateAttackTaxonomy, summarizeTaxonomy } from './posture/attack-taxonomy.js';
|
|
261
262
|
import { suppressByPastDecisions } from './posture/triage-memory.js';
|
|
262
263
|
import { suppressByIntent } from './posture/intent-context.js';
|
|
@@ -1862,7 +1863,22 @@ function scanRoutes(fp,raw){const cleaned=stripNoise(raw,fp);const lines=raw.spl
|
|
|
1862
1863
|
|
|
1863
1864
|
const LOGIC_PATTERNS=[
|
|
1864
1865
|
{regex:/Math\.random\s*\(\s*\)/g,vuln:"Weak Randomness",severity:"medium",cwe:"CWE-330",stride:"Spoofing",fix:"Use crypto.randomBytes or crypto.randomUUID for security-sensitive values.",code:"// BEFORE\nconst token = Math.random().toString(36);\n\n// AFTER\nconst token = crypto.randomBytes(32).toString('hex');"},
|
|
1865
|
-
|
|
1866
|
+
// Real false positive (customer report): `[^'"]` matches newlines, so the
|
|
1867
|
+
// "value" capture could pair the CLOSING quote of an unrelated string with
|
|
1868
|
+
// some OTHER quote found anywhere later in the file, fabricating a fake
|
|
1869
|
+
// "secret" spanning arbitrary source text in between. Confirmed
|
|
1870
|
+
// reproduction: `HUGGINGFACE_TOKEN = getpass.getpass("Enter your Hugging
|
|
1871
|
+
// Face token: ")` — the trigger word "token" occurs inside the human-
|
|
1872
|
+
// readable PROMPT text, immediately followed by ": " (satisfying the
|
|
1873
|
+
// `[:=]` requirement) and then that string's own closing quote (accepted
|
|
1874
|
+
// as the regex's opening quote); `[^'"]{3,}` then consumed everything up
|
|
1875
|
+
// to the next unrelated quote anywhere below, including subsequent
|
|
1876
|
+
// function definitions. getpass() collects a credential interactively at
|
|
1877
|
+
// runtime — the only literal in the source is the prompt, not a secret.
|
|
1878
|
+
// Excluding `\n` from the captured-value class confines a match to a
|
|
1879
|
+
// single physical line, which is what "hardcoded" is supposed to mean:
|
|
1880
|
+
// a real inline literal, not two coincidental quote characters far apart.
|
|
1881
|
+
{regex:/(?:password|secret|api_?key|token|auth)\s*[:=]\s*['"][^'"\n]{3,}['"]/gi,vuln:"Hardcoded Secret",severity:"critical",cwe:"CWE-798",stride:"Information Disclosure",kind:"secret",fix:"Use environment variables or a secrets manager.",code:"// BEFORE\nconst apiKey = 'sk-abc123';\n\n// AFTER\nconst apiKey = process.env.API_KEY;"},
|
|
1866
1882
|
{regex:/===?\s*['"](?:admin|root|password|123456|test|default)['"]/gi,vuln:"Hardcoded Credential Check",severity:"high",cwe:"CWE-798",stride:"Spoofing",kind:"secret",fix:"Use hashed password verification, never hardcoded strings.",code:"// BEFORE\nif (password === 'admin') grant();\n\n// AFTER\nconst valid = await bcrypt.compare(password, user.hashedPassword);"},
|
|
1867
1883
|
{regex:/if\s*\(\s*(?:fs\.existsSync|fs\.access|stat)\s*\([^)]+\)\s*\)[^{]*(?:readFile|writeFile|unlink|rename)/g,vuln:"Race Condition (TOCTOU)",severity:"medium",cwe:"CWE-367",stride:"Tampering",fix:"Use atomic operations instead of check-then-act patterns.",code:"// BEFORE\nif (fs.existsSync(p)) fs.unlinkSync(p);\n\n// AFTER\ntry { fs.unlinkSync(p); } catch(e) { if(e.code!=='ENOENT') throw e; }"},
|
|
1868
1884
|
{regex:/\.(?:isAdmin|isRole|role)\s*(?:===?\s*(?:true|['"]admin['"])|\)\s*\{)/g,vuln:"Inline Privilege Check",severity:"medium",cwe:"CWE-863",stride:"Elevation of Privilege",fix:"Use middleware-based RBAC instead of inline role checks.",code:"// BEFORE\nif (user.isAdmin) deleteAll();\n\n// AFTER\nrouter.delete('/all', requireRole('admin'), handler);"},
|
|
@@ -1889,7 +1905,19 @@ const LOGIC_PATTERNS=[
|
|
|
1889
1905
|
// ── Missing Bounds on Financial/Quantity Fields ──────────────────────────────
|
|
1890
1906
|
{regex:/(?:req|request|ctx)\s*(?:\.\s*body|\[\s*['"]body['"]\s*\])\s*[.[\s]*(?:quantity|amount|price|units|count|qty)\b(?![^;]{0,200}(?:Number\.isInteger|isNaN|Math\.abs|>=\s*1|>\s*0|>0|>=1|max\s*:))/g,vuln:"Missing Positive-Integer Validation on Financial Field",severity:"medium",cwe:"CWE-20",stride:"Tampering",fix:"Validate that financial/quantity fields are positive integers before processing. Negative values can create credit or reverse transactions.",code:"// BEFORE\nawait Order.create({ quantity: req.body.quantity, price: product.price });\n\n// AFTER\nconst qty = req.body.quantity;\nif (!Number.isInteger(qty) || qty < 1 || qty > 10000)\n return res.status(400).json({ error: 'quantity must be 1-10000' });\nawait Order.create({ quantity: qty, price: product.price });"},
|
|
1891
1907
|
// ── #22: Missing timeout on outbound HTTP requests (DoS) ─────────────────────
|
|
1892
|
-
|
|
1908
|
+
// Real false positive (customer report): this rule names JS/Node APIs
|
|
1909
|
+
// (fetch/axios/http.get) but carried no langScope, so it ran on every
|
|
1910
|
+
// language and matched the literal text "fetch(" wherever it occurred —
|
|
1911
|
+
// including a Python method DEFINITION (`def fetch(self, ...)`) and its
|
|
1912
|
+
// interface declaration and every call site, none of which perform an
|
|
1913
|
+
// outbound HTTP request in that language at all. The remediation shown was
|
|
1914
|
+
// also unconditionally JS (fetch/axios/node http), which made no sense on
|
|
1915
|
+
// a Python finding. Two fixes: scope to the languages these API names
|
|
1916
|
+
// actually mean something in, and exclude a `function fetch(...)`
|
|
1917
|
+
// declaration from matching a call-shaped rule — the same declaration-vs-
|
|
1918
|
+
// call confusion that made the Python case fire, reproduced once more
|
|
1919
|
+
// inside JS/TS itself (a local polyfill/wrapper named `fetch`).
|
|
1920
|
+
{regex:/(?<!\bfunction\s)(?:await\s+)?\b(?:fetch|axios\.(?:get|post|put|patch|delete|request)|http\.(?:get|request)|https\.(?:get|request)|got)\s*\(/gi,vuln:"Missing Timeout on Outbound HTTP Request (DoS)",severity:"medium",cwe:"CWE-400",stride:"Denial of Service",appliesTo:["server"],langScope:/\.(?:js|jsx|ts|tsx|mjs|cjs)$/i,fix:"Set a timeout on all outbound requests to prevent event-loop starvation from stalled upstreams.",code:"// fetch (Node 18+)\nconst resp = await fetch(url, { signal: AbortSignal.timeout(5000) });\n\n// axios\nawait axios.get(url, { timeout: 5000 });\n\n// node http\nconst req = http.get(url, cb);\nreq.setTimeout(5000, () => req.destroy());"},
|
|
1893
1921
|
// ── #24: ORM collection queries without pagination limit (DoS) ───────────────
|
|
1894
1922
|
{regex:/\.\s*(?:findAll|findMany|findAndCountAll)\s*\(\s*\{[^}]{0,500}\}/g,vuln:"ORM Collection Query Without Pagination Limit (DoS)",severity:"medium",cwe:"CWE-400",stride:"Denial of Service",appliesTo:["server"],fix:"Always set limit/take on collection queries to bound memory and DB load.",code:"const items = await Model.findAll({\n where: { userId: req.user.id },\n limit: Math.min(Number(req.query.limit) || 50, 100),\n offset: Number(req.query.offset) || 0,\n});"},
|
|
1895
1923
|
// ── #27: Missing audit log on sensitive mutations (Repudiation) ──────────────
|
|
@@ -10153,7 +10181,13 @@ function _deterministicFileTimings(timings) {
|
|
|
10153
10181
|
await _runAnnotator("_v3.calibrationDrift", () => { _v3.calibrationDrift = computeCalibrationDrift(scanRoot); });
|
|
10154
10182
|
// v3 next-gen: why-fired provenance is captured LAST so it reflects the
|
|
10155
10183
|
// final state of each finding after every other annotator has run.
|
|
10156
|
-
|
|
10184
|
+
// Real customer-reported inconsistency: this call passed a hardcoded `{}`
|
|
10185
|
+
// context, so `ctx.rulesetVersion` inside why-fired.js was always
|
|
10186
|
+
// undefined and every finding's `whyFired.scanner.rulesetVersion` read
|
|
10187
|
+
// `null` — even though the top-level attestation, computed from the same
|
|
10188
|
+
// `_effectiveRulesetVersion(scanRoot)` this file already calls at two
|
|
10189
|
+
// other sites, correctly named the real ruleset version. Thread it through.
|
|
10190
|
+
await _runAnnotator("annotateWhyFired", () => { annotateWhyFired(finalFindings, { rulesetVersion: (_effectiveRulesetVersion(scanRoot) || {}).version || null }); });
|
|
10157
10191
|
// SCA-SAST correlation: link SAST findings to SCA vulnerable packages
|
|
10158
10192
|
try{for(const f of finalFindings){if(!f.chain||!f.chain.length)continue;const src=f.chain[0]?.label||'';for(const sc of supplyChain){if(sc.type!=='vulnerable_dep')continue;if(src.includes(sc.name)||f.vuln?.toLowerCase().includes(sc.name)){f.scaCorrelation={osvId:sc.osvId,package:sc.name,version:sc.version,confirmed:true};sc.sastConfirmed=true;break;}}}}catch(_){}
|
|
10159
10193
|
// Multi-sink chain detection: group findings by source variable
|
|
@@ -10232,6 +10266,19 @@ function _deterministicFileTimings(timings) {
|
|
|
10232
10266
|
if (_attributions && _attributions.componentCount) persistAttributions(scanRoot, _attributions);
|
|
10233
10267
|
} catch (_) {}
|
|
10234
10268
|
}
|
|
10269
|
+
// AI-BOM: emit aibom.json (adversarial premortem Q2, 2026-09-07 — see
|
|
10270
|
+
// aibom.js's persistAIBOM header comment for why this was missing).
|
|
10271
|
+
// Previously only reachable via the CLI's --format aibom, which never
|
|
10272
|
+
// wrote it here, so module:aibom (eu-ai-act.json Art.11, nist-800-171-r3
|
|
10273
|
+
// .json 03.04.10, nist-ai-600-1.json MG-4.1-001) could never clear.
|
|
10274
|
+
if (process.env.AGENTIC_SECURITY_NO_AIBOM !== '1') {
|
|
10275
|
+
try {
|
|
10276
|
+
const _aibom = buildAIBOM({ components: annotatedComponents || [] }, fc, {});
|
|
10277
|
+
if (_aibom && (_aibom.models.length || _aibom.promptTemplates.length || _aibom.frameworks.length)) {
|
|
10278
|
+
persistAIBOM(scanRoot, _aibom);
|
|
10279
|
+
}
|
|
10280
|
+
} catch (_) {}
|
|
10281
|
+
}
|
|
10235
10282
|
// Attack taxonomy summary — aggregates ATT&CK / ATLAS / kill-chain
|
|
10236
10283
|
// distribution over all findings for the report layer.
|
|
10237
10284
|
if (process.env.AGENTIC_SECURITY_NO_ATTACK_TAX !== '1') {
|
|
@@ -10691,7 +10738,7 @@ function _deterministicFileTimings(timings) {
|
|
|
10691
10738
|
compliance: _complianceReport ? { stale: _complianceReport.summary?.stale || 0 } : null,
|
|
10692
10739
|
});
|
|
10693
10740
|
} // end if (!skipAnnotators) — FR-PROV-029
|
|
10694
|
-
return{entrypointInventory:_entrypointInventory,rootCauseSweep:_rootCauseSweep,proofCoverage:_proofCoverage,kevCatalog:kevCatalogMeta(),routes:dd(aR,r=>`${r.method}:${r.path}:${r.file}:${r.line}`),findings:finalFindings,sources:aSrc,sinks:aSink,sanitizers:aSan,filesScanned:files.length,crossFileCount:cf.length,logicVulns:aLogic,supplyChain,components:annotatedComponents,secrets:aSecrets,ciphers:{atRest:aCiphersRest,inTransit:aCiphersTransit},pfr,fc,suppressions:_getSuppressions(),_v3,_scanMeta,_engineErrors:{cppDataflowParseErrors:_cppDataflowParseErrors.value},annotatorErrors:_annotatorErrors,detectorErrors:_detectorErrors,executionProof:_executionProofSummary,logicClaims:_logicClaims,vulnHistory:_vulnHistory,threatModel:_threatModel,privacyFramework:_privacyFramework,privacyIrBacked:_privacyIrBacked,privacyTaxonomyVersion:_privacyTaxonomyVersion,sbomDiff:_sbomDiff,complianceReport:_complianceReport,exploitBundles:_exploitBundles,pqcPlan:_pqcPlan,licenseGraph:_licenseGraph,attributions:_attributions,attackTaxonomy:_taxonomySummary,scanHealth:_scanHealth,coverageLedger:_coverageLedger,lineageGraph:_lineageGraph,lineageStatus:_lineageStatus};}
|
|
10741
|
+
return{entrypointInventory:_entrypointInventory,rootCauseSweep:_rootCauseSweep,proofCoverage:_proofCoverage,kevCatalog:kevCatalogMeta(),routes:dd(aR,r=>`${r.method}:${r.path}:${r.file}:${r.line}`),findings:finalFindings,sources:aSrc,sinks:aSink,sanitizers:aSan,filesScanned:files.length,linesScanned:Object.values(fc).reduce((_n,_c)=>_n+(typeof _c==='string'?_c.split("\n").length:0),0),crossFileCount:cf.length,logicVulns:aLogic,supplyChain,components:annotatedComponents,secrets:aSecrets,ciphers:{atRest:aCiphersRest,inTransit:aCiphersTransit},pfr,fc,suppressions:_getSuppressions(),_v3,_scanMeta,_engineErrors:{cppDataflowParseErrors:_cppDataflowParseErrors.value},annotatorErrors:_annotatorErrors,detectorErrors:_detectorErrors,executionProof:_executionProofSummary,logicClaims:_logicClaims,vulnHistory:_vulnHistory,threatModel:_threatModel,privacyFramework:_privacyFramework,privacyIrBacked:_privacyIrBacked,privacyTaxonomyVersion:_privacyTaxonomyVersion,sbomDiff:_sbomDiff,complianceReport:_complianceReport,exploitBundles:_exploitBundles,pqcPlan:_pqcPlan,licenseGraph:_licenseGraph,attributions:_attributions,attackTaxonomy:_taxonomySummary,scanHealth:_scanHealth,coverageLedger:_coverageLedger,lineageGraph:_lineageGraph,lineageStatus:_lineageStatus};}
|
|
10695
10742
|
|
|
10696
10743
|
// Post-aggregation classification: every source becomes "unsafe"|"safe"; every sink becomes "confirmed"|"safe".
|
|
10697
10744
|
// Orphans (no finding linkage) are bucketed by file-local heuristic so the UI shows binary states only.
|
|
@@ -51,6 +51,62 @@ function _isValidMode(mode) {
|
|
|
51
51
|
return ASSURANCE_MODES.includes(mode);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// A real user hit this: `agentic-security ci <a directory downloaded as a
|
|
55
|
+
// GitHub zip, no .git present> --assurance strict` failed with the bare
|
|
56
|
+
// count this function used to produce alone — "1210 finding(s) have status
|
|
57
|
+
// outside [complete, uncommitted]" — with no indication that all 1210
|
|
58
|
+
// findings failed for the exact same, simple, fixable reason
|
|
59
|
+
// (`coordinator.js`'s `annotateGitProvenance` already knows and records it,
|
|
60
|
+
// in `finding.findingProvenance.limitations[0]`, but that reason never
|
|
61
|
+
// reached this message). A user reading "1210 problems" reasonably assumes
|
|
62
|
+
// their CODE has 1210 problems, not that their DIRECTORY isn't a git
|
|
63
|
+
// repository. This surfaces the dominant recorded reason instead of a bare
|
|
64
|
+
// count, and gives the two most common, fully-fixable reasons ("not a Git
|
|
65
|
+
// repository" from a zip download instead of `git clone`; a shallow CI
|
|
66
|
+
// checkout) a one-line, specific remedy — the same specificity the
|
|
67
|
+
// scanHealth branch above already gives for a stale-EPSS-cache failure.
|
|
68
|
+
function _provenanceFailureReason(badProvenance, totalFindings) {
|
|
69
|
+
const counts = new Map();
|
|
70
|
+
for (const f of badProvenance) {
|
|
71
|
+
const reason = f?.findingProvenance?.limitations?.[0] || f?.findingProvenance?.status || 'unknown';
|
|
72
|
+
counts.set(reason, (counts.get(reason) || 0) + 1);
|
|
73
|
+
}
|
|
74
|
+
const ranked = [...counts.entries()].sort((a, b) => b[1] - a[1]);
|
|
75
|
+
const [topReason, topCount] = ranked[0];
|
|
76
|
+
const allSameReason = ranked.length === 1;
|
|
77
|
+
const base = `strict mode requires complete finding provenance; ${badProvenance.length}/${totalFindings} finding(s) have status outside [complete, uncommitted]`;
|
|
78
|
+
|
|
79
|
+
if (topReason === 'not a Git repository' || topReason === 'repository state unavailable') {
|
|
80
|
+
return `${base} — reason: ${allSameReason ? 'all of them are' : `${topCount} of them are`} "${topReason}". ` +
|
|
81
|
+
`strict mode resolves finding provenance from git history, so it requires a real git repository ` +
|
|
82
|
+
`(a GitHub "Download ZIP" extracts without one). Run \`git init && git add -A && git commit -m init\` in ` +
|
|
83
|
+
`the scanned directory, point the scan at a real \`git clone\`, or drop --assurance strict for standard/advisory.`;
|
|
84
|
+
}
|
|
85
|
+
// engine.js's own comment on this branch: "unpinned_dep / no_lockfile and
|
|
86
|
+
// friends... describe the ABSENCE of a declaration, so 'which commit
|
|
87
|
+
// introduced this version' is not a question that has an answer to defer
|
|
88
|
+
// ... this is a known, disclosed limitation, not a bug... strict mode
|
|
89
|
+
// WILL fail on nearly any real project that has a package.json." That
|
|
90
|
+
// disclosure lived only in a source comment nobody hits this error reads —
|
|
91
|
+
// the README's own quickstart explicitly invites pointing --assurance
|
|
92
|
+
// strict at "your own project," where this is the single most likely
|
|
93
|
+
// outcome. Named here so the person who hits it learns it is expected and
|
|
94
|
+
// permanent, not something to keep investigating.
|
|
95
|
+
const supplyChainCount = ranked.filter(([r]) => r.startsWith('origin resolution does not apply to a')).reduce((s, [, n]) => s + n, 0);
|
|
96
|
+
if (supplyChainCount > 0 && supplyChainCount >= badProvenance.length / 2) {
|
|
97
|
+
return `${base} — ${supplyChainCount} of them describe an ABSENT dependency declaration ` +
|
|
98
|
+
`(an unpinned version, a missing lockfile) that has no origin commit to resolve, by design. This is a ` +
|
|
99
|
+
`known, permanent limitation: strict mode cannot pass while any are present, on any real project with ` +
|
|
100
|
+
`such a dependency. Fix the underlying SCA finding(s) (pin the version / add a lockfile) if you want ` +
|
|
101
|
+
`strict to pass, or use --assurance standard/advisory for a project you don't control the dependencies of.`;
|
|
102
|
+
}
|
|
103
|
+
if (allSameReason) {
|
|
104
|
+
return `${base} — all ${badProvenance.length} share the same reason: "${topReason}".`;
|
|
105
|
+
}
|
|
106
|
+
const breakdown = ranked.slice(0, 5).map(([reason, n]) => `${n}× "${reason}"`).join(', ');
|
|
107
|
+
return `${base} — breakdown: ${breakdown}${ranked.length > 5 ? ', …' : ''}.`;
|
|
108
|
+
}
|
|
109
|
+
|
|
54
110
|
/**
|
|
55
111
|
* @param {string} mode - one of ASSURANCE_MODES; invalid/missing degrades to the default.
|
|
56
112
|
* @param {object|null} scanHealth - the engine's computed scan.scanHealth (FR-206).
|
|
@@ -143,7 +199,7 @@ export function evaluateAssuranceMode(mode, scanHealth, findings = []) {
|
|
|
143
199
|
return {
|
|
144
200
|
ok: false,
|
|
145
201
|
mode: 'strict',
|
|
146
|
-
reason:
|
|
202
|
+
reason: _provenanceFailureReason(badProvenance, findings.length),
|
|
147
203
|
conditions,
|
|
148
204
|
};
|
|
149
205
|
}
|
|
@@ -151,4 +207,4 @@ export function evaluateAssuranceMode(mode, scanHealth, findings = []) {
|
|
|
151
207
|
return { ok: true, mode: 'strict', reason: null, conditions };
|
|
152
208
|
}
|
|
153
209
|
|
|
154
|
-
export const _internals = { _isValidMode };
|
|
210
|
+
export const _internals = { _isValidMode, _provenanceFailureReason };
|
|
@@ -266,6 +266,16 @@ export function buildScorecard(inputs) {
|
|
|
266
266
|
}
|
|
267
267
|
: null,
|
|
268
268
|
},
|
|
269
|
+
// Adversarial premortem Q7 (2026-09-07). See mappingCoverageOf's own
|
|
270
|
+
// header comment (posture/auditor-walkthrough.js) for why this exists:
|
|
271
|
+
// measuring the trend, not gating it — no threshold, no baseline, no
|
|
272
|
+
// pass/fail, since a drop is sometimes the correct outcome of an honest
|
|
273
|
+
// fix and sometimes a real regression, and only a human reading the
|
|
274
|
+
// diff each release can tell which. `[]` (never omitted) when the
|
|
275
|
+
// caller supplies nothing, so a reader can tell "measured, zero
|
|
276
|
+
// frameworks" from "this scorecard predates the metric" the same way
|
|
277
|
+
// every other section here distinguishes absence from zero.
|
|
278
|
+
complianceMappingCoverage: inputs.complianceMappingCoverage || [],
|
|
269
279
|
};
|
|
270
280
|
}
|
|
271
281
|
|
|
@@ -530,6 +540,30 @@ export function renderScorecardMarkdown(m) {
|
|
|
530
540
|
L.push('not a channel this measurement structurally cannot yet cover.');
|
|
531
541
|
L.push('');
|
|
532
542
|
}
|
|
543
|
+
if (Array.isArray(m.complianceMappingCoverage) && m.complianceMappingCoverage.length) {
|
|
544
|
+
L.push('## Compliance mapping coverage');
|
|
545
|
+
L.push('');
|
|
546
|
+
L.push('Adversarial premortem Q7 (2026-09-07): each fix to a category-error');
|
|
547
|
+
L.push('mapping (a control checking an artifact that evidences this scanner,');
|
|
548
|
+
L.push('not the target — see `03.03.08` in the NIST 800-171 coverage doc for the');
|
|
549
|
+
L.push('original instance) correctly SUBTRACTS a `mapsTo` entry. Nobody was');
|
|
550
|
+
L.push('tracking the cumulative effect release over release. This is not a');
|
|
551
|
+
L.push('gate — a drop is sometimes a correct, honest fix and sometimes a real');
|
|
552
|
+
L.push('regression, and only a human reading the diff each release can tell');
|
|
553
|
+
L.push('which — it exists so the trend is visible instead of assumed.');
|
|
554
|
+
L.push('');
|
|
555
|
+
L.push('| Framework | Controls with a live mapping | Share |');
|
|
556
|
+
L.push('| --- | --- | --- |');
|
|
557
|
+
for (const row of [...m.complianceMappingCoverage].sort((a, b) => String(a.id).localeCompare(String(b.id)))) {
|
|
558
|
+
L.push(`| ${row.id} | ${row.mappedCount}/${row.controlCount} | ${formatRate(row.mappedCount, row.controlCount)} |`);
|
|
559
|
+
}
|
|
560
|
+
L.push('');
|
|
561
|
+
L.push('"Live mapping" means the control carries at least one `family:`/`module:`/');
|
|
562
|
+
L.push('`rule:`/`graph:` entry, regardless of whether it would clear on any given');
|
|
563
|
+
L.push('scan — this counts what the engine CAN evidence, not what it evidenced');
|
|
564
|
+
L.push('this run.');
|
|
565
|
+
L.push('');
|
|
566
|
+
}
|
|
533
567
|
// PRD F12.6 — the honest scorecard publishes the LIMITS too, not only the
|
|
534
568
|
// rates. Three claims this project makes are only meaningful with their
|
|
535
569
|
// caveat attached, and each caveat was invisible before this section:
|
package/src/posture/aibom.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
// a labelled fixture set.
|
|
22
22
|
|
|
23
23
|
import * as crypto from 'node:crypto';
|
|
24
|
+
import { statePath, safeWriteState } from './state-dir.js';
|
|
24
25
|
|
|
25
26
|
// SDK / API endpoint detection — same family list as scanner/src/sast/llm.js
|
|
26
27
|
const HF_FROM_PRETRAINED_RE = /(?:Auto(?:Model|Tokenizer|Config|Processor|FeatureExtractor)|[A-Z][A-Za-z]*Model|[A-Z][A-Za-z]*Tokenizer)\.from_pretrained\s*\(\s*['"]([\w./-]+)['"](?:[^)]*?revision\s*=\s*['"]([\w]+)['"])?/g;
|
|
@@ -395,3 +396,24 @@ export function validateMLBOM(doc) {
|
|
|
395
396
|
}
|
|
396
397
|
return { ok: errors.length === 0, errors, checked: 'structural (required fields + ML-BOM component shape), NOT full JSON-Schema validation' };
|
|
397
398
|
}
|
|
399
|
+
|
|
400
|
+
// ─── Persistence (adversarial premortem Q2, 2026-09-07) ────────────────────
|
|
401
|
+
//
|
|
402
|
+
// `compliance-frameworks/*.json` has mapped `module:aibom` to `aibom.json`
|
|
403
|
+
// since those mappings were written (eu-ai-act.json Art.11, nist-800-171-r3
|
|
404
|
+
// .json 03.04.10, nist-ai-600-1.json MG-4.1-001), but nothing ever wrote it
|
|
405
|
+
// there automatically: `buildAIBOM` was only ever reachable through the CLI's
|
|
406
|
+
// `--format aibom`/`--format aibom-md` report emitters, which print to
|
|
407
|
+
// stdout (or wherever `--output` sends them) and never touch
|
|
408
|
+
// `.agentic-security/`. Three controls across three frameworks could never
|
|
409
|
+
// read 'present' via this leg, on any project, unless an operator happened
|
|
410
|
+
// to manually redirect `--format aibom` output to that exact path. Fixed the
|
|
411
|
+
// same way `license-attributions.js`'s `persistAttributions` and
|
|
412
|
+
// `threat-model.js`'s `persistAutoThreatModel` already are: a default-on
|
|
413
|
+
// (opt-out via AGENTIC_SECURITY_NO_AIBOM), best-effort write during every
|
|
414
|
+
// scan, wired in engine.js next to those two.
|
|
415
|
+
export function persistAIBOM(scanRoot, aibom) {
|
|
416
|
+
if (!aibom || typeof aibom !== 'object') return null;
|
|
417
|
+
safeWriteState(statePath(scanRoot, 'aibom.json'), JSON.stringify(aibom, null, 2));
|
|
418
|
+
return aibom;
|
|
419
|
+
}
|
|
@@ -119,6 +119,7 @@ export const ARTIFACT_REGISTRY = [
|
|
|
119
119
|
{ name: 'findings.csv', kind: 'file', classification: 'generated', retentionClass: 'scan' },
|
|
120
120
|
{ name: 'llm-cache', kind: 'dir', classification: 'generated', retentionClass: 'cache' },
|
|
121
121
|
{ name: 'fix-history', kind: 'dir', classification: 'generated', retentionClass: 'backup' },
|
|
122
|
+
{ name: 'verifier-runs', kind: 'dir', classification: 'generated', retentionClass: 'evidence', source: 'src/posture/verifier.js (recordVerifierRun)', note: 'Adversarial premortem Q1 (2026-09-07): one JSON record per `agentic-security verify` run, added so module:verifier (nist-800-171-r3.json 03.12.01, nist-csf-2.json RC.RP) has a real artifact to check for instead of a name in the ARTIFACT table nothing ever wrote.' },
|
|
122
123
|
{ name: 'fix-plans', kind: 'dir', classification: 'generated', retentionClass: 'scan' },
|
|
123
124
|
// The following were confirmed missing from the old hardcoded WIPE/
|
|
124
125
|
// WIPE_DIRS sets (A-10) and confirmed GENERATED by reading their write
|
|
@@ -135,6 +136,7 @@ export const ARTIFACT_REGISTRY = [
|
|
|
135
136
|
{ name: 'compliance-evidence.json', kind: 'file', classification: 'generated', retentionClass: 'evidence', confidential: true, source: 'posture/compliance-policy.js' },
|
|
136
137
|
{ name: 'compliance-evidence.md', kind: 'file', classification: 'generated', retentionClass: 'evidence', confidential: true, source: 'posture/compliance-policy.js' },
|
|
137
138
|
{ name: 'ATTRIBUTIONS.md', kind: 'file', classification: 'generated', retentionClass: 'scan', source: 'posture/license-attributions.js' },
|
|
139
|
+
{ name: 'aibom.json', kind: 'file', classification: 'generated', retentionClass: 'scan', source: 'posture/aibom.js (persistAIBOM)', note: 'Adversarial premortem Q2 (2026-09-07): module:aibom (eu-ai-act.json Art.11, nist-800-171-r3.json 03.04.10, nist-ai-600-1.json MG-4.1-001) mapped to this path since those mappings were written, but nothing wrote it here until now — buildAIBOM was only reachable via the CLI --format aibom emitter, which never touched .agentic-security/.' },
|
|
138
140
|
{ name: 'NOTICE', kind: 'file', classification: 'generated', retentionClass: 'scan', source: 'posture/license-attributions.js' },
|
|
139
141
|
{ name: 'accepted.json', kind: 'file', classification: 'generated', source: 'posture/suppressions.js (soft-accept save path)', note: 'already self-managing per-entry expiry (FR-1004-adjacent) — no additional class-level TTL' },
|
|
140
142
|
{ name: 'triage.json', kind: 'file', classification: 'generated', source: 'posture/triage.js (_save)' },
|
|
@@ -328,6 +328,96 @@ function _resolveOpenFindingMinSeverity(scanRoot, frameworkId) {
|
|
|
328
328
|
: OPEN_FINDING_MIN_SEVERITY;
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
+
// `module:` mapping vocabulary → the on-disk artifact(s) that evidence it.
|
|
332
|
+
// Hoisted to module scope (was previously re-declared on every control
|
|
333
|
+
// evaluated, a wasted allocation with no reader) and exported so
|
|
334
|
+
// `test/module-artifact-liveness.test.js` can check every entry here is
|
|
335
|
+
// actually written somewhere in scanner/src/, without keeping a second,
|
|
336
|
+
// driftable copy of this list. A table entry is either one path or an array
|
|
337
|
+
// of acceptable ones — see the `scan-history` comment below for why an array
|
|
338
|
+
// means "any of these satisfies it." A `.../` prefix marks a source-relative
|
|
339
|
+
// artifact (resolved against the scan root itself, not the state dir).
|
|
340
|
+
export const MODULE_ARTIFACTS = {
|
|
341
|
+
'sbom-diff': 'sbom-history/',
|
|
342
|
+
'license-attributions': 'ATTRIBUTIONS.md',
|
|
343
|
+
'threat-model-auto': 'threat-model.json',
|
|
344
|
+
'compliance-policy': 'compliance-evidence.json',
|
|
345
|
+
'fix-history': 'fix-history/log.json',
|
|
346
|
+
'privacy-taint': 'dpia.md',
|
|
347
|
+
'aibom': 'aibom.json',
|
|
348
|
+
'attack-taxonomy': 'last-scan.json',
|
|
349
|
+
// Two real spellings, both live in this codebase: security-trend.js and
|
|
350
|
+
// router.js read `scan-history.json` (a FILE), findings-memory.js uses
|
|
351
|
+
// `scan-history` (a DIRECTORY). Only the directory was listed here, so
|
|
352
|
+
// on a normal scan — which writes the .json — every control mapped to
|
|
353
|
+
// module:scan-history reported the artifact missing and could never
|
|
354
|
+
// clear. Five bundled frameworks were affected. An array means "any of
|
|
355
|
+
// these satisfies it", which is the honest reading: the control asks
|
|
356
|
+
// whether a scan history exists, not which shape it took.
|
|
357
|
+
'scan-history': ['scan-history.json', 'scan-history/'],
|
|
358
|
+
'watch-mode': 'watch-status.json',
|
|
359
|
+
'cve-alert-daemon': 'cve-alerts/',
|
|
360
|
+
'triage': 'triage.json',
|
|
361
|
+
'triage-memory': 'triage-memory.jsonl',
|
|
362
|
+
// Adversarial premortem Q1 (2026-09-07): this entry existed since the
|
|
363
|
+
// ARTIFACT table did, but nothing ever wrote verifier-runs/ — no control
|
|
364
|
+
// mapped to it could ever read 'present', on any project, permanently.
|
|
365
|
+
// `verifier.js`'s `recordVerifierRun` now writes one record per real
|
|
366
|
+
// `agentic-security verify` invocation, closing the gap for real.
|
|
367
|
+
'verifier': 'verifier-runs/',
|
|
368
|
+
'apply-fix': 'fix-history/log.json',
|
|
369
|
+
// REMOVED, deliberately (adversarial premortem P2.8 + Q1/Q2/Q6, 2026-09-07):
|
|
370
|
+
// 'integrity' (last-scan.json.sig), 'mcp-audit' (mcp-audit.log),
|
|
371
|
+
// 'calibration' (calibration-seed.json), 'holdout-eval'
|
|
372
|
+
// (holdout-eval.jsonl), 'sigstore-verify' (sigstore-attestations/, never
|
|
373
|
+
// written — see hipaa-security-rule.json's §164.312(c) removal note),
|
|
374
|
+
// 'pre-edit-bodyguard' (hooks/pre-edit-bodyguard.js), 'security-fixer'
|
|
375
|
+
// (agents/security-fixer.md), 'mcp-tools' (scanner/src/mcp/tools.js), and
|
|
376
|
+
// 'why-fired' (last-scan.json — its CONTENT is target-derived, unlike the
|
|
377
|
+
// others, but it evidences THIS TOOL's own detection provenance, not any
|
|
378
|
+
// property of the assessed system; adjudicated on eu-ai-act.json Art.13's
|
|
379
|
+
// actual text, see that control's evidence[] for the full reasoning) all
|
|
380
|
+
// evidence THIS SCANNER's own state, operation, or installed files —
|
|
381
|
+
// never the scanned project's — and are structurally incapable of validly
|
|
382
|
+
// backing any `module:` mapping, not just accidentally missing a writer.
|
|
383
|
+
// Every live mapsTo reference to any of them was removed and disclosed as
|
|
384
|
+
// an engine gap (see each affected framework file's evidence[] for the
|
|
385
|
+
// specific reasoning); they are removed from the vocabulary table itself,
|
|
386
|
+
// not merely un-referenced, so there is nothing left to copy-paste back
|
|
387
|
+
// in. `compliance-mapping-liveness.test.js`'s self-referential-module test
|
|
388
|
+
// remains as a permanent regression guard against the STRING key
|
|
389
|
+
// reappearing in a mapsTo array even without a table entry to source it
|
|
390
|
+
// from.
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
// Adversarial premortem Q7 (2026-09-07): each `03.03.08`-style fix
|
|
394
|
+
// (P2.8/Q1/Q2/Q6) subtracts a `mapsTo` entry to correct a category error —
|
|
395
|
+
// the right call every time it happened, but nobody was tracking the
|
|
396
|
+
// CUMULATIVE effect. "Always subtract, never invent" is correct engineering
|
|
397
|
+
// discipline that can still trend, unmeasured, toward a framework that
|
|
398
|
+
// automatically clears fewer and fewer controls each release, which looks
|
|
399
|
+
// to a buyer like the tool doing less over time even though every
|
|
400
|
+
// individual change made it more honest. This computes the number that
|
|
401
|
+
// makes the trend visible instead of assumed: how many of a framework's
|
|
402
|
+
// controls carry at least one LIVE mapsTo (family:/module:/rule:/graph:),
|
|
403
|
+
// regardless of whether that mapping would currently clear on any given
|
|
404
|
+
// scan — the question is "can this control ever be evidenced by this
|
|
405
|
+
// engine at all," not "did today's scan clear it." Pure and side-effect
|
|
406
|
+
// free, like the rest of this module's exports; the caller supplies the
|
|
407
|
+
// already-loaded framework object (see `scripts/scorecard.mjs` for the
|
|
408
|
+
// driver that loads every bundled framework and calls this once each).
|
|
409
|
+
export function mappingCoverageOf(fw) {
|
|
410
|
+
const controls = (fw && fw.controls) || [];
|
|
411
|
+
const controlCount = controls.length;
|
|
412
|
+
const mappedCount = controls.filter((c) => Array.isArray(c.mapsTo) && c.mapsTo.length > 0).length;
|
|
413
|
+
return {
|
|
414
|
+
id: fw && fw.id,
|
|
415
|
+
controlCount,
|
|
416
|
+
mappedCount,
|
|
417
|
+
mappedFraction: controlCount ? mappedCount / controlCount : null,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
331
421
|
export function evaluateFramework(scanRoot, fw, scan) {
|
|
332
422
|
const minSeverity = _resolveOpenFindingMinSeverity(scanRoot, fw && fw.id);
|
|
333
423
|
// CMP-2: last-scan.json (what this is actually handed in production) carries
|
|
@@ -521,46 +611,12 @@ export function evaluateFramework(scanRoot, fw, scan) {
|
|
|
521
611
|
anySignal = true;
|
|
522
612
|
} else if (m.startsWith('module:')) {
|
|
523
613
|
const mod = m.slice('module:'.length);
|
|
524
|
-
const ARTIFACT = {
|
|
525
|
-
'sbom-diff': 'sbom-history/',
|
|
526
|
-
'license-attributions': 'ATTRIBUTIONS.md',
|
|
527
|
-
'threat-model-auto': 'threat-model.json',
|
|
528
|
-
'compliance-policy': 'compliance-evidence.json',
|
|
529
|
-
'mcp-audit': 'mcp-audit.log',
|
|
530
|
-
'fix-history': 'fix-history/log.json',
|
|
531
|
-
'privacy-taint': 'dpia.md',
|
|
532
|
-
'aibom': 'aibom.json',
|
|
533
|
-
'attack-taxonomy': 'last-scan.json',
|
|
534
|
-
'why-fired': 'last-scan.json',
|
|
535
|
-
// Two real spellings, both live in this codebase: security-trend.js and
|
|
536
|
-
// router.js read `scan-history.json` (a FILE), findings-memory.js uses
|
|
537
|
-
// `scan-history` (a DIRECTORY). Only the directory was listed here, so
|
|
538
|
-
// on a normal scan — which writes the .json — every control mapped to
|
|
539
|
-
// module:scan-history reported the artifact missing and could never
|
|
540
|
-
// clear. Five bundled frameworks were affected. An array means "any of
|
|
541
|
-
// these satisfies it", which is the honest reading: the control asks
|
|
542
|
-
// whether a scan history exists, not which shape it took.
|
|
543
|
-
'scan-history': ['scan-history.json', 'scan-history/'],
|
|
544
|
-
'integrity': 'last-scan.json.sig',
|
|
545
|
-
'watch-mode': 'watch-status.json',
|
|
546
|
-
'cve-alert-daemon': 'cve-alerts/',
|
|
547
|
-
'triage': 'triage.json',
|
|
548
|
-
'triage-memory': 'triage-memory.jsonl',
|
|
549
|
-
'verifier': 'verifier-runs/',
|
|
550
|
-
'calibration': 'calibration-seed.json',
|
|
551
|
-
'holdout-eval': 'holdout-eval.jsonl',
|
|
552
|
-
'sigstore-verify': 'sigstore-attestations/',
|
|
553
|
-
'pre-edit-bodyguard': '.../hooks/pre-edit-bodyguard.js',
|
|
554
|
-
'apply-fix': 'fix-history/log.json',
|
|
555
|
-
'security-fixer': '.../agents/security-fixer.md',
|
|
556
|
-
'mcp-tools': '.../scanner/src/mcp/tools.js',
|
|
557
|
-
};
|
|
558
614
|
// A table entry is either one path or an array of acceptable ones. An
|
|
559
615
|
// array means the artifact has more than one real spelling in this
|
|
560
616
|
// codebase and any of them evidences the control; the FIRST is the
|
|
561
617
|
// canonical name used in the observation text when none is found, so
|
|
562
618
|
// the message still names something a reader can go create.
|
|
563
|
-
const target =
|
|
619
|
+
const target = MODULE_ARTIFACTS[mod];
|
|
564
620
|
const candidates = target == null ? [] : (Array.isArray(target) ? target : [target]);
|
|
565
621
|
// A '.../' sentinel marks a source-relative artifact (project source,
|
|
566
622
|
// e.g. a hook or agent file) — resolve it against the scan root itself.
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
"publisher": "California Legislature",
|
|
5
5
|
"license": "California statute (public)",
|
|
6
6
|
"url": "https://leginfo.legislature.ca.gov/faces/codes_displayText.xhtml?division=3.&part=4.&lawCode=CIV&title=1.81.5",
|
|
7
|
+
"sourceVerifiedAt": "2026-09-07",
|
|
8
|
+
"sourceVerificationNote": "Verified 2026-09-07 (WebFetch): resolves to California Civil Code Title 1.81.5 sections 1798.100-1798.145, correctly reflecting amendments through Stats. 2025, Ch. 67 (effective 2026-01-01). No content hash is pinned deliberately: this URL serves the CURRENT, continuously-amended text of codified law by design, not a fixed publication, hash-pinning would misrepresent a living legal source as a static one.",
|
|
7
9
|
"scope": "SELECTIVE SUBSET. 4 of the CCPA/CPRA obligations, chosen because a code scanner can produce evidence for them. The statute is far broader; the majority of its duties (notice, consumer request handling, contracts, retention policy) are organisational and are NOT represented here. Absence of a control is not a statement of compliance.",
|
|
8
10
|
"controlsDigest": "ebc1f708c329ab42",
|
|
9
11
|
"controlCount": 4,
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
"publisher": "European Parliament & Council",
|
|
5
5
|
"license": "EU law (Official Journal)",
|
|
6
6
|
"url": "https://eur-lex.europa.eu/eli/reg/2024/1689/oj",
|
|
7
|
+
"sourceVerificationAttempted": "2026-09-07",
|
|
8
|
+
"sourceVerificationNote": "Verification attempted 2026-09-07 and NOT completed: eur-lex.europa.eu returned HTTP 202 with an empty body and an x-amzn-waf-action: challenge header (AWS WAF bot challenge), both via the WebFetch tool and via a direct curl request with a standard browser User-Agent. This is recorded honestly as an unverified URL rather than a false confirmation. GDPR (the sibling eur-lex.europa.eu/eli/reg/2016/679/oj source, same domain) resolved successfully in the same session, so this appears to be per-request or per-path challenge behavior, not a durable domain-wide block. Re-attempt this check periodically; a human should confirm the URL manually until automated verification succeeds.",
|
|
7
9
|
"scope": "SELECTIVE SUBSET. 7 obligations drawn from the high-risk-system and GPAI articles where a code signal exists. The Act is far broader; conformity assessment, registration, human oversight and post-market monitoring are organisational and are NOT represented here.",
|
|
8
|
-
"controlsDigest": "
|
|
10
|
+
"controlsDigest": "f63f5763df191224",
|
|
9
11
|
"controlCount": 7,
|
|
10
12
|
"controls": [
|
|
11
13
|
{
|
|
@@ -53,11 +55,10 @@
|
|
|
53
55
|
"summary": "Record-keeping — automatic logging of system events for traceability.",
|
|
54
56
|
"codeTestable": "partial",
|
|
55
57
|
"evidence": [
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
+
"Scan history retained.",
|
|
59
|
+
"Removed module:mcp-audit after a second-review pass: mcp-audit.log records calls to THIS tool's own MCP server, i.e. how an agent used this scanner — it is not evidence that the assessed AI system logs its own events. Category error (self-referential: about this tool, not the target), same class as the 03.03.08 fix in the NIST 800-171 framework."
|
|
58
60
|
],
|
|
59
61
|
"mapsTo": [
|
|
60
|
-
"module:mcp-audit",
|
|
61
62
|
"module:scan-history"
|
|
62
63
|
]
|
|
63
64
|
},
|
|
@@ -66,11 +67,10 @@
|
|
|
66
67
|
"summary": "Transparency — instructions for use enable users to interpret the system's output correctly.",
|
|
67
68
|
"codeTestable": "partial",
|
|
68
69
|
"evidence": [
|
|
69
|
-
"why-fired
|
|
70
|
+
"Removed module:why-fired after adversarial premortem Q6 (2026-09-07, re-run): why-fired.js explains why THIS SCANNER's OWN detectors fired on a finding ('the user can see exactly what produced the finding without reading the scanner source' — its own header comment) — it is provenance for this tool's decisions, not evidence that the ASSESSED AI system gives its own users instructions to interpret ITS OWN output, which is what Article 13 actually asks. Every other control in this framework uses 'the system' to mean the assessed AI product (Art.9's risk management system, Art.12's event logging), so this reads Art.13 the same way rather than as an exception. Category error (self-referential: about this tool, not the target), same class as the 03.03.08 fix in the NIST 800-171 framework — this one was initially judged a borderline, undecided open question in the first premortem pass and is now adjudicated on a full reading of the actual control text and the actual module code.",
|
|
71
|
+
"Reported as an engine gap: whether an assessed AI system documents its own output for its own users is not something this engine, which never sees the assessed system's user-facing product surface, can evidence at all."
|
|
70
72
|
],
|
|
71
|
-
"mapsTo": [
|
|
72
|
-
"module:why-fired"
|
|
73
|
-
]
|
|
73
|
+
"mapsTo": []
|
|
74
74
|
},
|
|
75
75
|
{
|
|
76
76
|
"id": "Art.14",
|
|
@@ -78,10 +78,9 @@
|
|
|
78
78
|
"codeTestable": "partial",
|
|
79
79
|
"evidence": [
|
|
80
80
|
"Fix application requires confirm:true.",
|
|
81
|
-
"
|
|
81
|
+
"Removed module:pre-edit-bodyguard after a second-review pass: that file is this scanning tool's OWN installed hook, not an artifact of the assessed AI system. Its presence only shows this tool's plugin is installed, never that the assessed system itself permits human override or interruption. Category error (self-referential: about this tool, not the target)."
|
|
82
82
|
],
|
|
83
83
|
"mapsTo": [
|
|
84
|
-
"module:pre-edit-bodyguard",
|
|
85
84
|
"module:apply-fix"
|
|
86
85
|
]
|
|
87
86
|
},
|
|
@@ -90,13 +89,10 @@
|
|
|
90
89
|
"summary": "Accuracy, robustness, cybersecurity — appropriate level of accuracy and resilience.",
|
|
91
90
|
"codeTestable": "partial",
|
|
92
91
|
"evidence": [
|
|
93
|
-
"
|
|
94
|
-
"
|
|
92
|
+
"No automated signal in this engine after a second-review pass removed both prior mappings. module:calibration and module:holdout-eval pointed at THIS scanner's own ML calibration corpus and held-out evaluation labels (calibration-seed.json, holdout-eval.jsonl) — files that describe how accurately this tool's OWN detectors are calibrated, not whether the assessed AI system has an appropriate level of accuracy, robustness or cybersecurity. Category error (self-referential: about this tool, not the target), same class as the 03.03.08 fix in the NIST 800-171 framework.",
|
|
93
|
+
"Reported as an engine gap: an AI system's own accuracy/robustness testing is code-observable in principle (evaluation harnesses, robustness test suites in the assessed system's own repo), but no detector here looks for that in the SCANNED project."
|
|
95
94
|
],
|
|
96
|
-
"mapsTo": [
|
|
97
|
-
"module:calibration",
|
|
98
|
-
"module:holdout-eval"
|
|
99
|
-
]
|
|
95
|
+
"mapsTo": []
|
|
100
96
|
}
|
|
101
97
|
]
|
|
102
98
|
}
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
"publisher": "European Parliament & Council",
|
|
5
5
|
"license": "EU law (Official Journal)",
|
|
6
6
|
"url": "https://eur-lex.europa.eu/eli/reg/2016/679/oj",
|
|
7
|
+
"sourceVerifiedAt": "2026-09-07",
|
|
8
|
+
"sourceVerificationNote": "Verified 2026-09-07 (WebFetch): eur-lex.europa.eu/eli/reg/2016/679/oj resolves to the consolidated text of Regulation (EU) 2016/679 (GDPR), confirming Articles 5, 25, 32, 33, 35 and 44 (the six represented here) match the mapped summaries. No content hash is pinned deliberately: EUR-Lex serves the CURRENT consolidated text and its page template can change independently of the legal text itself, so a page hash would drift on template changes that carry no legal-content change and would need constant, meaningless re-pinning — hash-pinning would misrepresent a living legal source as a static publication.",
|
|
7
9
|
"scope": "SELECTIVE SUBSET. 6 articles where a code scanner can produce evidence (security of processing, data minimisation, DPIA inputs). GDPR has 99 articles; lawful basis, data-subject rights, transfers and records of processing are organisational and are NOT represented here.",
|
|
8
|
-
"controlsDigest": "
|
|
10
|
+
"controlsDigest": "cd018b0daad4fa66",
|
|
9
11
|
"controlCount": 6,
|
|
10
12
|
"controls": [
|
|
11
13
|
{
|
|
@@ -67,11 +69,10 @@
|
|
|
67
69
|
"codeTestable": "partial",
|
|
68
70
|
"evidence": [
|
|
69
71
|
"Fix history retained.",
|
|
70
|
-
"
|
|
72
|
+
"Removed module:mcp-audit after a second-review pass: mcp-audit.log records calls to THIS tool's own MCP server, not the controller's own breach-detection or notification workflow. Category error (self-referential: about this tool, not the target)."
|
|
71
73
|
],
|
|
72
74
|
"mapsTo": [
|
|
73
|
-
"module:fix-history"
|
|
74
|
-
"module:mcp-audit"
|
|
75
|
+
"module:fix-history"
|
|
75
76
|
]
|
|
76
77
|
},
|
|
77
78
|
{
|
|
@@ -79,10 +80,11 @@
|
|
|
79
80
|
"summary": "Data protection impact assessment (DPIA) for high-risk processing.",
|
|
80
81
|
"codeTestable": "partial",
|
|
81
82
|
"evidence": [
|
|
82
|
-
"DPIA artifact present at .agentic-security/dpia.md."
|
|
83
|
+
"DPIA artifact present at .agentic-security/dpia.md (privacy-taint.js's emitDpiaArtifact).",
|
|
84
|
+
"Fixed a malformed mapsTo entry (adversarial premortem Q2, 2026-09-07): 'module:privacy-taint:emitDpiaArtifact' does not match any key in the ARTIFACT vocabulary table (auditor-walkthrough.js parses everything after 'module:' as one literal key, with no ':'-suffix syntax) — this control could never read 'present' via this leg, on any project, since the mapping shipped. Corrected to the real key, 'module:privacy-taint'."
|
|
83
85
|
],
|
|
84
86
|
"mapsTo": [
|
|
85
|
-
"module:privacy-taint
|
|
87
|
+
"module:privacy-taint"
|
|
86
88
|
]
|
|
87
89
|
}
|
|
88
90
|
]
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
"publisher": "US Department of Health and Human Services",
|
|
5
5
|
"license": "US Federal regulation (public)",
|
|
6
6
|
"url": "https://www.ecfr.gov/current/title-45/subtitle-A/subchapter-C/part-164",
|
|
7
|
+
"sourceVerifiedAt": "2026-09-07",
|
|
8
|
+
"sourceVerificationNote": "Verified 2026-09-07: the WebFetch tool was blocked by eCFR's automated bot detection (redirected to unblock.federalregister.gov, a CAPTCHA challenge). Verified instead via a direct HTTP request (curl with a standard browser User-Agent, HTTP 200), confirming sections 164.302/304/306/308 (the HIPAA Security Rule administrative safeguards) are present at this URL. No content hash is pinned: eCFR explicitly serves the CURRENT version of the regulation by design (the URL path itself says \"current\"), so hash-pinning would treat a living regulatory text as a fixed publication.",
|
|
7
9
|
"scope": "SELECTIVE SUBSET. 8 of the Security Rule technical safeguards. Administrative and physical safeguards are outside what a code scanner can observe and are NOT represented here.",
|
|
8
|
-
"controlsDigest": "
|
|
10
|
+
"controlsDigest": "4be9e2264a574e33",
|
|
9
11
|
"controlCount": 8,
|
|
10
12
|
"controls": [
|
|
11
13
|
{
|
|
@@ -73,10 +75,10 @@
|
|
|
73
75
|
"summary": "Audit controls — record and examine activity in systems containing PHI.",
|
|
74
76
|
"codeTestable": "partial",
|
|
75
77
|
"evidence": [
|
|
76
|
-
"
|
|
78
|
+
"Fix history present and hash-chained.",
|
|
79
|
+
"Removed module:mcp-audit after a second-review pass: mcp-audit.log records calls to THIS tool's own MCP server, not activity in the covered entity's own systems containing PHI. Category error (self-referential: about this tool, not the target)."
|
|
77
80
|
],
|
|
78
81
|
"mapsTo": [
|
|
79
|
-
"module:mcp-audit",
|
|
80
82
|
"module:fix-history"
|
|
81
83
|
]
|
|
82
84
|
},
|
|
@@ -85,13 +87,11 @@
|
|
|
85
87
|
"summary": "Integrity — PHI not altered or destroyed in an unauthorized manner.",
|
|
86
88
|
"codeTestable": "partial",
|
|
87
89
|
"evidence": [
|
|
88
|
-
"last-scan.json
|
|
89
|
-
"Sigstore
|
|
90
|
+
"Removed module:integrity after a second-review pass: last-scan.json.sig is this scanner signing its OWN scan output, not evidence that the covered entity's PHI is protected from unauthorized alteration or destruction. Category error (self-referential: about this tool, not the target), same class as the 03.03.08 fix in the NIST 800-171 framework.",
|
|
91
|
+
"Removed module:sigstore-verify after a second-review pass (adversarial premortem Q1, 2026-09-07): sigstore-attestations/ is never written anywhere in this codebase — the real Sigstore verification logic (scanner/src/sca/sigstore-verify.js) caches results in a per-user home-directory cache (~/.claude/agentic-security/sigstore-cache/), shared across every project scanned on that machine, by design (content-addressed by package sha256, correctly reused rather than duplicated per project). Its own annotation call is fire-and-forget async in engine.js, not awaited, so even a project-relative summary write would not reliably complete before the scan process exits — building one blind, without first fixing that completion guarantee, was judged a bigger and riskier change than this task's scope, so this mapping is disclosed as a gap instead of built unverified.",
|
|
92
|
+
"Reported as an engine gap: whether PHI integrity is protected against unauthorized alteration or destruction is code-observable in principle (checksums, WORM storage, tamper-evident logging in the covered entity's own application code), but no detector here evidences that in the SCANNED project."
|
|
90
93
|
],
|
|
91
|
-
"mapsTo": [
|
|
92
|
-
"module:integrity",
|
|
93
|
-
"module:sigstore-verify"
|
|
94
|
-
]
|
|
94
|
+
"mapsTo": []
|
|
95
95
|
},
|
|
96
96
|
{
|
|
97
97
|
"id": "§164.312(e)",
|