@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/bin/agentic-security.js +5 -1
  3. package/dist/4970.index.js +108 -2
  4. package/dist/agentic-security.mjs +2 -2
  5. package/dist/agentic-security.mjs.sha256 +1 -1
  6. package/dist/compliance-frameworks/ccpa.json +2 -0
  7. package/dist/compliance-frameworks/eu-ai-act.json +12 -16
  8. package/dist/compliance-frameworks/gdpr.json +8 -6
  9. package/dist/compliance-frameworks/hipaa-security-rule.json +9 -9
  10. package/dist/compliance-frameworks/nist-800-171-r3.json +6 -5
  11. package/dist/compliance-frameworks/nist-ai-600-1.json +6 -4
  12. package/dist/compliance-frameworks/nist-csf-2.json +6 -5
  13. package/dist/compliance-frameworks/nist-privacy-1-1.json +20 -10
  14. package/dist/compliance-frameworks/owasp-asvs-5.json +2 -0
  15. package/dist/compliance-frameworks/owasp-llm-top-10.json +10 -11
  16. package/package.json +4 -3
  17. package/src/engine.js +46 -14
  18. package/src/pipeline/assurance-mode.js +108 -2
  19. package/src/posture/accuracy-scorecard.js +34 -0
  20. package/src/posture/aibom.js +22 -0
  21. package/src/posture/artifact-registry.js +2 -0
  22. package/src/posture/auditor-walkthrough.js +91 -35
  23. package/src/posture/compliance-frameworks/ccpa.json +2 -0
  24. package/src/posture/compliance-frameworks/eu-ai-act.json +12 -16
  25. package/src/posture/compliance-frameworks/gdpr.json +8 -6
  26. package/src/posture/compliance-frameworks/hipaa-security-rule.json +9 -9
  27. package/src/posture/compliance-frameworks/nist-800-171-r3.json +6 -5
  28. package/src/posture/compliance-frameworks/nist-ai-600-1.json +6 -4
  29. package/src/posture/compliance-frameworks/nist-csf-2.json +6 -5
  30. package/src/posture/compliance-frameworks/nist-privacy-1-1.json +20 -10
  31. package/src/posture/compliance-frameworks/owasp-asvs-5.json +2 -0
  32. package/src/posture/compliance-frameworks/owasp-llm-top-10.json +10 -11
  33. package/src/posture/verifier.js +70 -0
package/CHANGELOG.md CHANGED
@@ -10,6 +10,124 @@
10
10
 
11
11
 
12
12
 
13
+ ## 0.148.4 - Adversarial premortem re-run on the 0.148.2/0.148.3 --assurance strict fix: two real defects found and fixed
14
+
15
+ 0.148.2's fix for a confusing `--assurance strict` failure was itself put through an adversarial
16
+ premortem rather than trusted on its own say-so, the same discipline applied to the compliance-
17
+ framework work earlier. It found the message-building logic held up in the common single-reason
18
+ case, but had two real defects when a scan had more than one kind of provenance problem at once —
19
+ which, on a real project, is not an edge case.
20
+
21
+ 1. **Multiple concurrent reasons silently collapsed to one.** A non-git directory with an
22
+ unpinned dependency produces BOTH a `'not a Git repository'` reason (every SAST/secrets/logic
23
+ finding) AND a `'origin resolution does not apply to...'` reason (the unpinned-dependency SCA
24
+ finding) on the very same scan — not hypothetically; `engine.js`'s two SCA populations that get
25
+ real git-history resolution are filtered to `type === 'vulnerable_dep'` only, so `unpinned_dep`/
26
+ `no_lockfile` never pass through the git-repo check at all. The message-building code picked
27
+ whichever reason had the most findings and silently dropped the other, so a user could fix the
28
+ reported problem (initialize git), rerun, and hit a second wall the tool had full information
29
+ about on the very first run but never mentioned — a milder recurrence of the exact "the tool
30
+ knew and didn't tell me" complaint the original fix existed to close. `_provenanceFailureReason`
31
+ (`scanner/src/pipeline/assurance-mode.js`) now reports every real category present, not just the
32
+ largest one.
33
+
34
+ 2. **Two structurally different SCA gaps were given the same, wrong advice.** `unpinned_dep`/
35
+ `no_lockfile` genuinely have no origin commit to resolve (they describe an absent declaration) —
36
+ correctly labeled a permanent, by-design limitation. But `cdn_no_integrity`/`dynamic_require`
37
+ both carry a real file:line (a specific `<script src>` tag or `require(...)` call someone wrote)
38
+ that a future resolver update genuinely could walk; `engine.js`'s provenance-stamping loop
39
+ previously gave all four types the identical limitation string, so the message-building code told
40
+ a user with a `cdn_no_integrity` finding to stop investigating a resolvable coverage gap because
41
+ it looked identical to a truly unresolvable one. `engine.js` now gives the two classes distinct,
42
+ honest limitation strings.
43
+
44
+ Both landed with new regression tests (`scanner/test/assurance-mode.test.js`) covering the exact
45
+ multi-reason interaction that exposed the first defect, and the cdn/dynamic-require case for the
46
+ second. No behavior changed for the single-reason case most scans will actually hit.
47
+
48
+ ## 0.148.3 - Dependency currency fix; supersedes 0.148.2, which never published
49
+
50
+ The `v0.148.2` tag was pushed but its release workflow's dependency-currency gate failed on a
51
+ clean CI runner — `@types/node` (dev-only, scanner and ide/vscode) was one patch behind the
52
+ latest published version, with no advisory and no legitimate reason to hold it. `npm publish`
53
+ never ran for 0.148.2; nothing shipped under that version. Fixed by upgrading `@types/node` to
54
+ `26.5.0` in both trees. No other change from 0.148.2's own content — see that entry below for
55
+ what actually shipped.
56
+
57
+ ## 0.148.2 - A confusing --assurance strict failure made specific and actionable; adversarial premortem re-run on NIST 800-171
58
+
59
+ A user ran the README's own 5-minute quickstart command and got a confusing, uninformative
60
+ failure — see the last item in this entry for the fix. Separately, 0.148.1's compliance-framework
61
+ fixes were put through a second, independent adversarial pass rather than trusted on their own
62
+ say-so. It found the individual fixes held up, but surfaced two live defects the first pass
63
+ missed entirely, plus six gaps in how the fixes themselves were verified and disclosed.
64
+
65
+ 1. `module:verifier` and `module:sigstore-verify` were referenced by real controls in four
66
+ bundled frameworks, but nothing ever wrote either artifact — worse than a self-referential
67
+ mapping (which can at least clear, dishonestly), a mapping that can never clear at all, on
68
+ any project, permanently. `verifier.js` now writes a real record on every
69
+ `agentic-security verify` run, closing `module:verifier` for real — proven end-to-end:
70
+ NIST 800-171 `03.12.01` flips from missing to present after a real `verify` invocation.
71
+ `module:sigstore-verify` is disclosed as an engine gap instead: its real producer is a
72
+ fire-and-forget async annotation with no reliable completion point to hang a writer off,
73
+ a bigger fix than this pass's scope.
74
+
75
+ 2. A new writer-existence test (`scanner/test/module-artifact-liveness.test.js`) checks every
76
+ `module:` artifact resolves to a real writer or a real repo file, not just a name on a list.
77
+ Running it once found two more real, previously-unknown defects the manual premortem review
78
+ missed: a malformed `module:privacy-taint:emitDpiaArtifact` typo in `gdpr.json` (the
79
+ evaluator parses everything after `module:` as one literal key — no `:`-suffix syntax
80
+ exists — so this control could never clear), and a dead `module:exploitability-probability`
81
+ reference in `nist-csf-2.json` (that annotator writes no standalone artifact; removed as
82
+ redundant with the surviving `module:attack-taxonomy` leg on the same control). Both fixed.
83
+ The eight structurally self-referential `module:` entries found across this and the prior
84
+ release are now removed from the vocabulary table itself, not merely left unreferenced, so
85
+ there is nothing left to copy-paste back into a future mapping.
86
+
87
+ 3. `aibom.json` now auto-persists on every scan. It was only ever reachable through the CLI's
88
+ `--format aibom` report emitter, which prints to stdout — never to `.agentic-security/` —
89
+ so `module:aibom` (EU AI Act Art.11, NIST 800-171 `03.04.10`, NIST AI 600-1 `MG-4.1-001`)
90
+ could never clear on any project unless an operator manually redirected the CLI's output to
91
+ that exact path.
92
+
93
+ 4. The scoring-threshold provenance caveat added in 0.148.1 (that NIST 800-171's deep-attestation
94
+ scanner shares AI 600-1's unvalidated weights and thresholds) lived only in a doc and a
95
+ source docstring — never in the actual generated attestation markdown a customer would
96
+ submit. It now prints inline in that document whenever the catalog being scored isn't the
97
+ framework the scoring engine was built for.
98
+
99
+ 5. The held-out anchor set added in 0.148.1 to guard the 97 hand-authored `codeTestable`
100
+ ratings covered only 3 of the 9 controls that release's own rationale-expansion pass
101
+ actually touched — the other 6 had no drift protection despite being exactly the work the
102
+ gate exists to protect. Expanded to cover all 9.
103
+
104
+ 6. `module:why-fired` (EU AI Act Art.13), left an open, undecided question in 0.148.1's own
105
+ premortem, is now adjudicated: on a full reading of the control text (every other control in
106
+ the same framework uses "the system" to mean the assessed AI product) and of `why-fired.js`'s
107
+ actual purpose (explains this scanner's own detection provenance, not the assessed system's),
108
+ it is the same category error as the other eight — the artifact's content being genuinely
109
+ target-derived doesn't make the claim it was asked to back correct.
110
+
111
+ 7. `docs/SCORECARD.md` now tracks, per bundled framework, how many controls carry at least one
112
+ live mapping — not as a gate (a drop is sometimes a correct, honest fix and sometimes a real
113
+ regression, and only a human reading the diff each release can tell which), but so the
114
+ cumulative effect of "always subtract a bad mapping, never invent one" is visible instead of
115
+ assumed fine.
116
+
117
+ 8. The PRD's own implementation record (`NIST-800-171r3-PRD.md`) now documents this pass.
118
+
119
+ 9. `agentic-security ci <path> --assurance strict` on a directory with no git history — the
120
+ exact result of running the README's own quickstart command against a GitHub "Download ZIP"
121
+ extraction instead of a `git clone` — used to fail with only a bare count: "1210 finding(s)
122
+ have status outside [complete, uncommitted]." The scanner already knew and recorded the real
123
+ reason (`finding.findingProvenance.limitations[0] = 'not a Git repository'`), it just never
124
+ reached the message a user actually sees. The assurance-gate failure now names the dominant
125
+ recorded reason and, for the two most common shapes, gives a specific fix: no git history
126
+ ("run `git init && git add -A && git commit`, or scan a real `git clone`") and an unpinned
127
+ dependency / missing lockfile ("this is a permanent, by-design limitation — fix the SCA
128
+ finding or use `--assurance standard`"). `docs/walkthroughs/assurance-modes.md` documents
129
+ both (`scanner/src/pipeline/assurance-mode.js`).
130
+
13
131
 
14
132
  ## 0.148.1 - Six real false-positive fixes from a customer bug report, two report-consistency fixes
15
133
 
@@ -1875,7 +1875,7 @@ async function cmdVerify(args) {
1875
1875
  process.env.AGENTIC_SECURITY_VERIFY_LIVE = '1';
1876
1876
  process.env.AGENTIC_SECURITY_VERIFY_TARGET = targetFlag;
1877
1877
  }
1878
- const { annotateVerifierVerdicts, verifierCoverageSummary } = await import('../src/posture/verifier.js');
1878
+ const { annotateVerifierVerdicts, verifierCoverageSummary, recordVerifierRun } = await import('../src/posture/verifier.js');
1879
1879
  const filter = args.flags.finding ? findings.filter(f => f.id === args.flags.finding || f.stableId === args.flags.finding) : findings;
1880
1880
  if (!filter.length) {
1881
1881
  console.error(`No matching findings (use --finding <id>).`);
@@ -1901,6 +1901,10 @@ async function cmdVerify(args) {
1901
1901
  const sum = verifierCoverageSummary(filter);
1902
1902
  console.log(`Verified ${filter.length} finding(s):`);
1903
1903
  for (const [k, v] of Object.entries(sum)) console.log(` ${k}: ${v}`);
1904
+ // Adversarial premortem Q1: a durable record that a security assessment of
1905
+ // findings actually happened, so module:verifier has something real to
1906
+ // point at (see verifier.js's recordVerifierRun header comment).
1907
+ recordVerifierRun(scanRoot, { findingCount: filter.length, live: liveFlag, target: targetFlag || null, summary: sum });
1904
1908
  if (args.flags.verbose || args.flags.finding) {
1905
1909
  for (const f of filter) {
1906
1910
  console.log(` ${f.file}:${f.line} ${f.vuln}`);
@@ -65,6 +65,112 @@ function _isValidMode(mode) {
65
65
  return ASSURANCE_MODES.includes(mode);
66
66
  }
67
67
 
68
+ // A real user hit this: `agentic-security ci <a directory downloaded as a
69
+ // GitHub zip, no .git present> --assurance strict` failed with the bare
70
+ // count this function used to produce alone — "1210 finding(s) have status
71
+ // outside [complete, uncommitted]" — with no indication that all 1210
72
+ // findings failed for the exact same, simple, fixable reason
73
+ // (`coordinator.js`'s `annotateGitProvenance` already knows and records it,
74
+ // in `finding.findingProvenance.limitations[0]`, but that reason never
75
+ // reached this message). A user reading "1210 problems" reasonably assumes
76
+ // their CODE has 1210 problems, not that their DIRECTORY isn't a git
77
+ // repository. This surfaces the dominant recorded reason instead of a bare
78
+ // count, and gives the two most common, fully-fixable reasons ("not a Git
79
+ // repository" from a zip download instead of `git clone`; a shallow CI
80
+ // checkout) a one-line, specific remedy — the same specificity the
81
+ // scanHealth branch above already gives for a stale-EPSS-cache failure.
82
+ function _provenanceFailureReason(badProvenance, totalFindings) {
83
+ const counts = new Map();
84
+ for (const f of badProvenance) {
85
+ const reason = f?.findingProvenance?.limitations?.[0] || f?.findingProvenance?.status || 'unknown';
86
+ counts.set(reason, (counts.get(reason) || 0) + 1);
87
+ }
88
+ const ranked = [...counts.entries()].sort((a, b) => b[1] - a[1]);
89
+ const base = `strict mode requires complete finding provenance; ${badProvenance.length}/${totalFindings} finding(s) have status outside [complete, uncommitted]`;
90
+
91
+ const gitReasons = ranked.filter(([r]) => r === 'not a Git repository' || r === 'repository state unavailable');
92
+ const gitCount = gitReasons.reduce((s, [, n]) => s + n, 0);
93
+ // engine.js's own comment on this branch: "unpinned_dep / no_lockfile...
94
+ // describe the ABSENCE of a declaration, so 'which commit introduced this
95
+ // version' is not a question that has an answer to defer ... this is a
96
+ // known, disclosed limitation, not a bug... strict mode WILL fail on
97
+ // nearly any real project that has a package.json." That disclosure lived
98
+ // only in a source comment nobody hits this error reads — the README's
99
+ // own quickstart explicitly invites pointing --assurance strict at "your
100
+ // own project," where this is the single most likely outcome. Named here
101
+ // so the person who hits it learns it is expected and permanent, not
102
+ // something to keep investigating. This prefix is deliberately narrower
103
+ // than "every non-vulnerable_dep supply-chain entry" — engine.js's
104
+ // provenance-stamping loop only uses it for unpinned_dep/no_lockfile,
105
+ // which genuinely have no origin commit; cdn_no_integrity/dynamic_require
106
+ // carry a real file:line and get a DIFFERENT string precisely so they
107
+ // never land in this "permanent, give up" bucket (adversarial premortem
108
+ // R2, 2026-09-07 — conflating the two told a user a resolvable coverage
109
+ // gap was an unfixable, by-design limitation).
110
+ const supplyChainReasons = ranked.filter(([r]) => r.startsWith('origin resolution does not apply to a'));
111
+ const supplyChainCount = supplyChainReasons.reduce((s, [, n]) => s + n, 0);
112
+ const knownReasonSet = new Set([...gitReasons, ...supplyChainReasons].map(([r]) => r));
113
+ const otherReasons = ranked.filter(([r]) => !knownReasonSet.has(r));
114
+ const otherCount = badProvenance.length - gitCount - supplyChainCount;
115
+ const knownCategoryCount = (gitCount > 0 ? 1 : 0) + (supplyChainCount > 0 ? 1 : 0);
116
+
117
+ // Exactly one KNOWN category, and nothing outside it — the shape every
118
+ // caller before this fix assumed was the only shape, and the one every
119
+ // existing test was written against. Kept as tight, single-topic prose
120
+ // rather than the multi-segment form below.
121
+ if (knownCategoryCount === 0) {
122
+ if (otherReasons.length === 1) {
123
+ return `${base} — all ${badProvenance.length} share the same reason: "${otherReasons[0][0]}".`;
124
+ }
125
+ const breakdown = otherReasons.slice(0, 5).map(([reason, n]) => `${n}× "${reason}"`).join(', ');
126
+ return `${base} — breakdown: ${breakdown}${otherReasons.length > 5 ? ', …' : ''}.`;
127
+ }
128
+ if (knownCategoryCount === 1 && otherCount === 0) {
129
+ if (gitCount > 0) {
130
+ const gitReasonNames = gitReasons.map(([r]) => `"${r}"`).join(' and ');
131
+ return `${base} — reason: ${gitCount === badProvenance.length ? 'all of them are' : `${gitCount} of them are`} ${gitReasonNames}. ` +
132
+ `strict mode resolves finding provenance from git history, so it requires a real git repository ` +
133
+ `(a GitHub "Download ZIP" extracts without one). Run \`git init && git add -A && git commit -m init\` in ` +
134
+ `the scanned directory, point the scan at a real \`git clone\`, or drop --assurance strict for standard/advisory.`;
135
+ }
136
+ return `${base} — ${supplyChainCount} of them describe an ABSENT dependency declaration ` +
137
+ `(an unpinned version, a missing lockfile) that has no origin commit to resolve, by design. This is a ` +
138
+ `known, permanent limitation: strict mode cannot pass while any are present, on any real project with ` +
139
+ `such a dependency. Fix the underlying SCA finding(s) (pin the version / add a lockfile) if you want ` +
140
+ `strict to pass, or use --assurance standard/advisory for a project you don't control the dependencies of.`;
141
+ }
142
+
143
+ // Two or more independently-blocking categories on the SAME scan — the
144
+ // defect this closes (adversarial premortem R1, 2026-09-07): the old
145
+ // code picked whichever category had the most findings and silently
146
+ // dropped every other one, so a user could "fix" the reported problem,
147
+ // rerun, and hit a second wall the first run already had full information
148
+ // about but never mentioned — the same "the tool knew and didn't tell me"
149
+ // complaint this whole function exists to fix, recurring in a milder form.
150
+ const segments = [];
151
+ if (gitCount > 0) {
152
+ const gitReasonNames = gitReasons.map(([r]) => `"${r}"`).join(' and ');
153
+ segments.push(`${gitCount} of them are ${gitReasonNames} (strict mode requires a real git repository — ` +
154
+ `run \`git init && git add -A && git commit\`, or scan a real \`git clone\`)`);
155
+ }
156
+ if (supplyChainCount > 0) {
157
+ segments.push(`${supplyChainCount} of them describe an ABSENT dependency declaration (unpinned version / ` +
158
+ `missing lockfile) with no origin commit to resolve — a known, permanent limitation, not something a ` +
159
+ `rerun will fix`);
160
+ }
161
+ if (otherCount > 0) {
162
+ if (otherReasons.length === 1) {
163
+ segments.push(`${otherCount} share the reason "${otherReasons[0][0]}"`);
164
+ } else {
165
+ const breakdown = otherReasons.slice(0, 5).map(([reason, n]) => `${n}× "${reason}"`).join(', ');
166
+ segments.push(`${otherCount} break down as: ${breakdown}${otherReasons.length > 5 ? ', …' : ''}`);
167
+ }
168
+ }
169
+ return `${base} — MULTIPLE distinct reasons, not just one: ${segments.join('; ')}. Every category above must ` +
170
+ `be resolved for strict to pass (or drop to --assurance standard/advisory) — fixing only one will surface ` +
171
+ `the next on your following run.`;
172
+ }
173
+
68
174
  /**
69
175
  * @param {string} mode - one of ASSURANCE_MODES; invalid/missing degrades to the default.
70
176
  * @param {object|null} scanHealth - the engine's computed scan.scanHealth (FR-206).
@@ -157,7 +263,7 @@ function evaluateAssuranceMode(mode, scanHealth, findings = []) {
157
263
  return {
158
264
  ok: false,
159
265
  mode: 'strict',
160
- reason: `strict mode requires complete finding provenance; ${badProvenance.length} finding(s) have status outside [complete, uncommitted]`,
266
+ reason: _provenanceFailureReason(badProvenance, findings.length),
161
267
  conditions,
162
268
  };
163
269
  }
@@ -165,7 +271,7 @@ function evaluateAssuranceMode(mode, scanHealth, findings = []) {
165
271
  return { ok: true, mode: 'strict', reason: null, conditions };
166
272
  }
167
273
 
168
- const _internals = { _isValidMode };
274
+ const _internals = { _isValidMode, _provenanceFailureReason };
169
275
 
170
276
 
171
277
  /***/ })