@clear-capabilities/agentic-security-scanner 0.148.1 → 0.148.4
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 +118 -0
- package/bin/agentic-security.js +5 -1
- package/dist/4970.index.js +108 -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 +4 -3
- package/src/engine.js +46 -14
- package/src/pipeline/assurance-mode.js +108 -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/verifier.js +70 -0
|
@@ -51,6 +51,112 @@ 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 base = `strict mode requires complete finding provenance; ${badProvenance.length}/${totalFindings} finding(s) have status outside [complete, uncommitted]`;
|
|
76
|
+
|
|
77
|
+
const gitReasons = ranked.filter(([r]) => r === 'not a Git repository' || r === 'repository state unavailable');
|
|
78
|
+
const gitCount = gitReasons.reduce((s, [, n]) => s + n, 0);
|
|
79
|
+
// engine.js's own comment on this branch: "unpinned_dep / no_lockfile...
|
|
80
|
+
// describe the ABSENCE of a declaration, so 'which commit introduced this
|
|
81
|
+
// version' is not a question that has an answer to defer ... this is a
|
|
82
|
+
// known, disclosed limitation, not a bug... strict mode WILL fail on
|
|
83
|
+
// nearly any real project that has a package.json." That disclosure lived
|
|
84
|
+
// only in a source comment nobody hits this error reads — the README's
|
|
85
|
+
// own quickstart explicitly invites pointing --assurance strict at "your
|
|
86
|
+
// own project," where this is the single most likely outcome. Named here
|
|
87
|
+
// so the person who hits it learns it is expected and permanent, not
|
|
88
|
+
// something to keep investigating. This prefix is deliberately narrower
|
|
89
|
+
// than "every non-vulnerable_dep supply-chain entry" — engine.js's
|
|
90
|
+
// provenance-stamping loop only uses it for unpinned_dep/no_lockfile,
|
|
91
|
+
// which genuinely have no origin commit; cdn_no_integrity/dynamic_require
|
|
92
|
+
// carry a real file:line and get a DIFFERENT string precisely so they
|
|
93
|
+
// never land in this "permanent, give up" bucket (adversarial premortem
|
|
94
|
+
// R2, 2026-09-07 — conflating the two told a user a resolvable coverage
|
|
95
|
+
// gap was an unfixable, by-design limitation).
|
|
96
|
+
const supplyChainReasons = ranked.filter(([r]) => r.startsWith('origin resolution does not apply to a'));
|
|
97
|
+
const supplyChainCount = supplyChainReasons.reduce((s, [, n]) => s + n, 0);
|
|
98
|
+
const knownReasonSet = new Set([...gitReasons, ...supplyChainReasons].map(([r]) => r));
|
|
99
|
+
const otherReasons = ranked.filter(([r]) => !knownReasonSet.has(r));
|
|
100
|
+
const otherCount = badProvenance.length - gitCount - supplyChainCount;
|
|
101
|
+
const knownCategoryCount = (gitCount > 0 ? 1 : 0) + (supplyChainCount > 0 ? 1 : 0);
|
|
102
|
+
|
|
103
|
+
// Exactly one KNOWN category, and nothing outside it — the shape every
|
|
104
|
+
// caller before this fix assumed was the only shape, and the one every
|
|
105
|
+
// existing test was written against. Kept as tight, single-topic prose
|
|
106
|
+
// rather than the multi-segment form below.
|
|
107
|
+
if (knownCategoryCount === 0) {
|
|
108
|
+
if (otherReasons.length === 1) {
|
|
109
|
+
return `${base} — all ${badProvenance.length} share the same reason: "${otherReasons[0][0]}".`;
|
|
110
|
+
}
|
|
111
|
+
const breakdown = otherReasons.slice(0, 5).map(([reason, n]) => `${n}× "${reason}"`).join(', ');
|
|
112
|
+
return `${base} — breakdown: ${breakdown}${otherReasons.length > 5 ? ', …' : ''}.`;
|
|
113
|
+
}
|
|
114
|
+
if (knownCategoryCount === 1 && otherCount === 0) {
|
|
115
|
+
if (gitCount > 0) {
|
|
116
|
+
const gitReasonNames = gitReasons.map(([r]) => `"${r}"`).join(' and ');
|
|
117
|
+
return `${base} — reason: ${gitCount === badProvenance.length ? 'all of them are' : `${gitCount} of them are`} ${gitReasonNames}. ` +
|
|
118
|
+
`strict mode resolves finding provenance from git history, so it requires a real git repository ` +
|
|
119
|
+
`(a GitHub "Download ZIP" extracts without one). Run \`git init && git add -A && git commit -m init\` in ` +
|
|
120
|
+
`the scanned directory, point the scan at a real \`git clone\`, or drop --assurance strict for standard/advisory.`;
|
|
121
|
+
}
|
|
122
|
+
return `${base} — ${supplyChainCount} of them describe an ABSENT dependency declaration ` +
|
|
123
|
+
`(an unpinned version, a missing lockfile) that has no origin commit to resolve, by design. This is a ` +
|
|
124
|
+
`known, permanent limitation: strict mode cannot pass while any are present, on any real project with ` +
|
|
125
|
+
`such a dependency. Fix the underlying SCA finding(s) (pin the version / add a lockfile) if you want ` +
|
|
126
|
+
`strict to pass, or use --assurance standard/advisory for a project you don't control the dependencies of.`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Two or more independently-blocking categories on the SAME scan — the
|
|
130
|
+
// defect this closes (adversarial premortem R1, 2026-09-07): the old
|
|
131
|
+
// code picked whichever category had the most findings and silently
|
|
132
|
+
// dropped every other one, so a user could "fix" the reported problem,
|
|
133
|
+
// rerun, and hit a second wall the first run already had full information
|
|
134
|
+
// about but never mentioned — the same "the tool knew and didn't tell me"
|
|
135
|
+
// complaint this whole function exists to fix, recurring in a milder form.
|
|
136
|
+
const segments = [];
|
|
137
|
+
if (gitCount > 0) {
|
|
138
|
+
const gitReasonNames = gitReasons.map(([r]) => `"${r}"`).join(' and ');
|
|
139
|
+
segments.push(`${gitCount} of them are ${gitReasonNames} (strict mode requires a real git repository — ` +
|
|
140
|
+
`run \`git init && git add -A && git commit\`, or scan a real \`git clone\`)`);
|
|
141
|
+
}
|
|
142
|
+
if (supplyChainCount > 0) {
|
|
143
|
+
segments.push(`${supplyChainCount} of them describe an ABSENT dependency declaration (unpinned version / ` +
|
|
144
|
+
`missing lockfile) with no origin commit to resolve — a known, permanent limitation, not something a ` +
|
|
145
|
+
`rerun will fix`);
|
|
146
|
+
}
|
|
147
|
+
if (otherCount > 0) {
|
|
148
|
+
if (otherReasons.length === 1) {
|
|
149
|
+
segments.push(`${otherCount} share the reason "${otherReasons[0][0]}"`);
|
|
150
|
+
} else {
|
|
151
|
+
const breakdown = otherReasons.slice(0, 5).map(([reason, n]) => `${n}× "${reason}"`).join(', ');
|
|
152
|
+
segments.push(`${otherCount} break down as: ${breakdown}${otherReasons.length > 5 ? ', …' : ''}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return `${base} — MULTIPLE distinct reasons, not just one: ${segments.join('; ')}. Every category above must ` +
|
|
156
|
+
`be resolved for strict to pass (or drop to --assurance standard/advisory) — fixing only one will surface ` +
|
|
157
|
+
`the next on your following run.`;
|
|
158
|
+
}
|
|
159
|
+
|
|
54
160
|
/**
|
|
55
161
|
* @param {string} mode - one of ASSURANCE_MODES; invalid/missing degrades to the default.
|
|
56
162
|
* @param {object|null} scanHealth - the engine's computed scan.scanHealth (FR-206).
|
|
@@ -143,7 +249,7 @@ export function evaluateAssuranceMode(mode, scanHealth, findings = []) {
|
|
|
143
249
|
return {
|
|
144
250
|
ok: false,
|
|
145
251
|
mode: 'strict',
|
|
146
|
-
reason:
|
|
252
|
+
reason: _provenanceFailureReason(badProvenance, findings.length),
|
|
147
253
|
conditions,
|
|
148
254
|
};
|
|
149
255
|
}
|
|
@@ -151,4 +257,4 @@ export function evaluateAssuranceMode(mode, scanHealth, findings = []) {
|
|
|
151
257
|
return { ok: true, mode: 'strict', reason: null, conditions };
|
|
152
258
|
}
|
|
153
259
|
|
|
154
|
-
export const _internals = { _isValidMode };
|
|
260
|
+
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)",
|
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
"publisher": "NIST",
|
|
5
5
|
"license": "public-domain (US Federal publication)",
|
|
6
6
|
"url": "https://csrc.nist.gov/pubs/sp/800/171/r3/final",
|
|
7
|
+
"sourceDoi": "https://doi.org/10.6028/NIST.SP.800-171r3",
|
|
8
|
+
"sourcePdfSha256": "3e4631df8b5d61f40a6e542b52779ef30ddbbfff31e09214fa94ad6e6f5e6d08",
|
|
9
|
+
"sourceVerificationNote": "Verified 2026-09-07: the CSRC landing page URL resolves and states title \"Protecting Controlled Unclassified Information in Nonfederal Systems and Organizations\", Rev. 3, published May 2024. The DOI resolves (302) to the PDF at nvlpubs.nist.gov, whose own embedded XMP metadata (pdfx:DOI, pdfx:Comments) confirms NIST.SP.800-171r3. sourcePdfSha256 is the SHA-256 of that exact downloaded PDF, so a future re-check can detect if NIST republishes under the same URL/DOI with different content. This does not verify the CSV control export against the PDF text control-by-control — see scripts/nist-800-171/README for that gap.",
|
|
7
10
|
"note": "Control text is generated from docs/standards/NIST_SP_800_171r3_Controls.csv by scripts/nist-800-171/build-catalog.py. The codeTestable rating is THIS ENGINE'S judgment, not NIST's. Unlike the AI 600-1 workbook, the 800-171 export rates no control for testability. Ratings and their rationales live in scripts/nist-800-171/code-testability.json.",
|
|
8
11
|
"scope": "FULL CATALOGUE — all 97 requirements of NIST SP 800-171 Rev. 3 are carried, deliberately including those this engine cannot assess, because omitting them would read as coverage. Automated evidence is PARTIAL and unevenly distributed: 16 requirements are rated code-testable, 38 partial, and 43 are organisational, physical or personnel controls NOT represented by any automated signal: all of Awareness and Training, Personnel Security and Physical Protection, and most of Incident Response, Maintenance and Media Protection. Those are forced to 'partial' with an explicit disclosure and can never read as satisfied. This is not a CMMC assessment and produces no SPRS score.",
|
|
9
|
-
"controlsDigest": "
|
|
12
|
+
"controlsDigest": "927295dcd1594189",
|
|
10
13
|
"controlCount": 97,
|
|
11
14
|
"controls": [
|
|
12
15
|
{
|
|
@@ -330,10 +333,8 @@
|
|
|
330
333
|
"summary": "Protection of Audit Information: a. Protect audit information and audit logging tools from unauthorized access, modification, and deletion. b. Authorize access to management of audit logging functionality to only a subset of…",
|
|
331
334
|
"codeTestable": "partial",
|
|
332
335
|
"evidence": [
|
|
333
|
-
"
|
|
334
|
-
|
|
335
|
-
"mapsTo": [
|
|
336
|
-
"module:integrity"
|
|
336
|
+
"No automated signal in this engine — this tool can sign and verify its OWN scan output (module:integrity), but that evidences this scanner's integrity, not whether the SCANNED PROJECT protects its own audit logs from unauthorized access, modification or deletion. No detector here inspects the scanned codebase for log-signing, tamper-evident storage, or access controls on audit data. Removed after a second-review pass found the original mapping was a category error (self-referential: about this tool, not the target).",
|
|
337
|
+
"Reported as an engine gap: this requirement is code-observable in principle (a detector could look for tamper-evident logging libraries or access-controlled log stores in application code), but no detector here decides it."
|
|
337
338
|
]
|
|
338
339
|
},
|
|
339
340
|
{
|
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
"publisher": "NIST",
|
|
5
5
|
"license": "public-domain (US Federal publication)",
|
|
6
6
|
"url": "https://nvlpubs.nist.gov/nistpubs/ai/NIST.AI.600-1.pdf",
|
|
7
|
+
"sourceDoi": "https://doi.org/10.6028/NIST.AI.600-1",
|
|
8
|
+
"sourcePdfSha256": "6e73620ab6b64e90ef2c04bf0e0d6246185a2f4b1b13cab0df494496cff89b6a",
|
|
9
|
+
"sourceVerificationNote": "Verified 2026-09-07: the doi.org DOI resolves (302) to this exact url, and the downloaded PDF's own embedded XMP metadata (pdfx:Category, dc:title) confirms \"NIST AI 600-1\" / \"Artificial Intelligence Risk Management Framework: Generative Artificial Intelligence Profile\". sourcePdfSha256 is the SHA-256 of that exact downloaded PDF, so a future re-check can detect if NIST republishes under the same URL/DOI with different content.",
|
|
7
10
|
"scope": "SELECTIVE SUBSET. 6 actions from the Generative AI Profile with a code-observable signal. The profile is far larger; governance, provenance and incident-response actions are organisational and are NOT represented here. The full control catalogue is built and gated separately by scripts/nist-compliance/build-catalog.py.",
|
|
8
|
-
"controlsDigest": "
|
|
11
|
+
"controlsDigest": "6e7cc01c5ba8ae22",
|
|
9
12
|
"controlCount": 6,
|
|
10
13
|
"controls": [
|
|
11
14
|
{
|
|
@@ -84,11 +87,10 @@
|
|
|
84
87
|
"summary": "Establish content provenance / authenticity controls.",
|
|
85
88
|
"codeTestable": "partial",
|
|
86
89
|
"evidence": [
|
|
87
|
-
"
|
|
88
|
-
"
|
|
90
|
+
"ATTRIBUTIONS.md generated.",
|
|
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 — see the identical removal note on hipaa-security-rule.json's §164.312(c) for the full reasoning (home-directory cache by design, fire-and-forget async producer, not a quick artifact-path fix). Content-provenance / authenticity controls for model loads remain only partially evidenced by this control's surviving module:license-attributions leg."
|
|
89
92
|
],
|
|
90
93
|
"mapsTo": [
|
|
91
|
-
"module:sigstore-verify",
|
|
92
94
|
"module:license-attributions"
|
|
93
95
|
]
|
|
94
96
|
}
|