@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.
Files changed (35) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/bin/agentic-security.js +5 -1
  3. package/dist/4970.index.js +58 -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 +5 -4
  17. package/src/engine.js +51 -4
  18. package/src/pipeline/assurance-mode.js +58 -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/threat-model.js +20 -3
  34. package/src/posture/verifier.js +70 -0
  35. package/src/sast/python-sinks.js +24 -1
package/CHANGELOG.md CHANGED
@@ -10,6 +10,156 @@
10
10
 
11
11
 
12
12
 
13
+ ## 0.148.3 - Dependency currency fix; supersedes 0.148.2, which never published
14
+
15
+ The `v0.148.2` tag was pushed but its release workflow's dependency-currency gate failed on a
16
+ clean CI runner — `@types/node` (dev-only, scanner and ide/vscode) was one patch behind the
17
+ latest published version, with no advisory and no legitimate reason to hold it. `npm publish`
18
+ never ran for 0.148.2; nothing shipped under that version. Fixed by upgrading `@types/node` to
19
+ `26.5.0` in both trees. No other change from 0.148.2's own content — see that entry below for
20
+ what actually shipped.
21
+
22
+ ## 0.148.2 - A confusing --assurance strict failure made specific and actionable; adversarial premortem re-run on NIST 800-171
23
+
24
+ A user ran the README's own 5-minute quickstart command and got a confusing, uninformative
25
+ failure — see the last item in this entry for the fix. Separately, 0.148.1's compliance-framework
26
+ fixes were put through a second, independent adversarial pass rather than trusted on their own
27
+ say-so. It found the individual fixes held up, but surfaced two live defects the first pass
28
+ missed entirely, plus six gaps in how the fixes themselves were verified and disclosed.
29
+
30
+ 1. `module:verifier` and `module:sigstore-verify` were referenced by real controls in four
31
+ bundled frameworks, but nothing ever wrote either artifact — worse than a self-referential
32
+ mapping (which can at least clear, dishonestly), a mapping that can never clear at all, on
33
+ any project, permanently. `verifier.js` now writes a real record on every
34
+ `agentic-security verify` run, closing `module:verifier` for real — proven end-to-end:
35
+ NIST 800-171 `03.12.01` flips from missing to present after a real `verify` invocation.
36
+ `module:sigstore-verify` is disclosed as an engine gap instead: its real producer is a
37
+ fire-and-forget async annotation with no reliable completion point to hang a writer off,
38
+ a bigger fix than this pass's scope.
39
+
40
+ 2. A new writer-existence test (`scanner/test/module-artifact-liveness.test.js`) checks every
41
+ `module:` artifact resolves to a real writer or a real repo file, not just a name on a list.
42
+ Running it once found two more real, previously-unknown defects the manual premortem review
43
+ missed: a malformed `module:privacy-taint:emitDpiaArtifact` typo in `gdpr.json` (the
44
+ evaluator parses everything after `module:` as one literal key — no `:`-suffix syntax
45
+ exists — so this control could never clear), and a dead `module:exploitability-probability`
46
+ reference in `nist-csf-2.json` (that annotator writes no standalone artifact; removed as
47
+ redundant with the surviving `module:attack-taxonomy` leg on the same control). Both fixed.
48
+ The eight structurally self-referential `module:` entries found across this and the prior
49
+ release are now removed from the vocabulary table itself, not merely left unreferenced, so
50
+ there is nothing left to copy-paste back into a future mapping.
51
+
52
+ 3. `aibom.json` now auto-persists on every scan. It was only ever reachable through the CLI's
53
+ `--format aibom` report emitter, which prints to stdout — never to `.agentic-security/` —
54
+ so `module:aibom` (EU AI Act Art.11, NIST 800-171 `03.04.10`, NIST AI 600-1 `MG-4.1-001`)
55
+ could never clear on any project unless an operator manually redirected the CLI's output to
56
+ that exact path.
57
+
58
+ 4. The scoring-threshold provenance caveat added in 0.148.1 (that NIST 800-171's deep-attestation
59
+ scanner shares AI 600-1's unvalidated weights and thresholds) lived only in a doc and a
60
+ source docstring — never in the actual generated attestation markdown a customer would
61
+ submit. It now prints inline in that document whenever the catalog being scored isn't the
62
+ framework the scoring engine was built for.
63
+
64
+ 5. The held-out anchor set added in 0.148.1 to guard the 97 hand-authored `codeTestable`
65
+ ratings covered only 3 of the 9 controls that release's own rationale-expansion pass
66
+ actually touched — the other 6 had no drift protection despite being exactly the work the
67
+ gate exists to protect. Expanded to cover all 9.
68
+
69
+ 6. `module:why-fired` (EU AI Act Art.13), left an open, undecided question in 0.148.1's own
70
+ premortem, is now adjudicated: on a full reading of the control text (every other control in
71
+ the same framework uses "the system" to mean the assessed AI product) and of `why-fired.js`'s
72
+ actual purpose (explains this scanner's own detection provenance, not the assessed system's),
73
+ it is the same category error as the other eight — the artifact's content being genuinely
74
+ target-derived doesn't make the claim it was asked to back correct.
75
+
76
+ 7. `docs/SCORECARD.md` now tracks, per bundled framework, how many controls carry at least one
77
+ live mapping — not as a gate (a drop is sometimes a correct, honest fix and sometimes a real
78
+ regression, and only a human reading the diff each release can tell which), but so the
79
+ cumulative effect of "always subtract a bad mapping, never invent one" is visible instead of
80
+ assumed fine.
81
+
82
+ 8. The PRD's own implementation record (`NIST-800-171r3-PRD.md`) now documents this pass.
83
+
84
+ 9. `agentic-security ci <path> --assurance strict` on a directory with no git history — the
85
+ exact result of running the README's own quickstart command against a GitHub "Download ZIP"
86
+ extraction instead of a `git clone` — used to fail with only a bare count: "1210 finding(s)
87
+ have status outside [complete, uncommitted]." The scanner already knew and recorded the real
88
+ reason (`finding.findingProvenance.limitations[0] = 'not a Git repository'`), it just never
89
+ reached the message a user actually sees. The assurance-gate failure now names the dominant
90
+ recorded reason and, for the two most common shapes, gives a specific fix: no git history
91
+ ("run `git init && git add -A && git commit`, or scan a real `git clone`") and an unpinned
92
+ dependency / missing lockfile ("this is a permanent, by-design limitation — fix the SCA
93
+ finding or use `--assurance standard`"). `docs/walkthroughs/assurance-modes.md` documents
94
+ both (`scanner/src/pipeline/assurance-mode.js`).
95
+
96
+
97
+ ## 0.148.1 - Six real false-positive fixes from a customer bug report, two report-consistency fixes
98
+
99
+ A developer ran the scanner on two of their own applications and sent back a detailed,
100
+ reproducible bug report. All six false positives traced back to the same defect class:
101
+ a loosely-bounded regex matching an identifier or text substring without real syntax
102
+ awareness. None were caught by unit tests, because a fixture an author writes by hand is
103
+ shaped the way the author expects the code to look; these were found by a real user reading
104
+ the lines a real scan cited.
105
+
106
+ 1. SQL injection fired on "Selected"/"selected". Case-insensitive `SELECT` had no word
107
+ boundary, so it matched the first six letters of "Selected" (an exception message) and
108
+ "selected" (an ordinary list variable). The rule's own name claimed "assigned to variable"
109
+ but never checked for an assignment; both fixed (`scanner/src/sast/python-sinks.js`).
110
+
111
+ 2. A Python method literally named `fetch(self, ...)` was reported as a missing-timeout
112
+ HTTP-DoS finding, at its interface declaration, its implementation, and its call site, with
113
+ a hardcoded JavaScript remediation. The rule named JS-only APIs (fetch/axios/http.get) but
114
+ carried no `langScope`, so it matched the raw text "fetch(" in any language. Fixed with a
115
+ language scope and a declaration-vs-call exclusion (`scanner/src/engine.js`).
116
+
117
+ 3. `HUGGINGFACE_TOKEN = getpass.getpass("Enter your Hugging Face token: ")` was flagged
118
+ Critical, Hardcoded Secret. The captured-value character class matched newlines, so once
119
+ the word "token" inside the human-readable PROMPT happened to be followed by `: "`, the
120
+ "secret" spanned past that string's own closing quote into the next function definition.
121
+ Excluding newlines from the class confines a match to one physical line
122
+ (`scanner/src/engine.js`).
123
+
124
+ 4. `bundle.publisher_domain` and `bundle.published_at` were read as a message-queue producer
125
+ boundary. `(?:kafka|pubsub|sqs|sns)\.produce|\.publish|\.sendMessage` parses as three
126
+ TOP-LEVEL alternatives; only the first is scoped to a queue library, so `.publish` alone
127
+ matched the first 8 characters of "publisher"/"published" with no queue library and no
128
+ method call in sight. The identical defect existed one line above for `queue-consumer`
129
+ (`.subscribe`/`.receiveMessage`, e.g. an RxJS Observable) and is fixed the same way:
130
+ the method names moved inside the shared prefix group, and a trailing `(` is now required
131
+ (`scanner/src/posture/threat-model.js`).
132
+
133
+ 5. `self.session = requests.Session()` was classified as an authenticated user-session asset.
134
+ The pattern checked only the LHS variable name, never the RHS constructor. A negative
135
+ lookahead now excludes `requests.Session`/`aiohttp.ClientSession`/`httpx.Client`/
136
+ `httpx.AsyncClient` (`scanner/src/posture/threat-model.js`).
137
+
138
+ 6. `scanned.lines` was always 0 and a finding's `whyFired.scanner.rulesetVersion` was always
139
+ `null`, in both cases despite the top-level scan doing the right thing elsewhere. Two dead-
140
+ wiring bugs, not detector defects: `linesScanned` was read in `report/index.js` but never
141
+ assigned anywhere in the engine, and `annotateWhyFired(finalFindings, {})` was called with a
142
+ hardcoded empty context even though the engine already computes the real ruleset version two
143
+ other places in the same function (`scanner/src/engine.js`).
144
+
145
+ `scanner/src/posture/threat-model.js` had zero test coverage before this release
146
+ (`scanner/test/threat-model.test.js` is new). Each fix also carries a negative case proving the
147
+ real, intended finding is still reported — a precision fix that silences a genuine positive is
148
+ worse than the false positive it replaced.
149
+
150
+ Confirmed, by reading the code, NOT a bug: the duplicate `stableId` the report also flagged
151
+ (same id on the interface declaration and the implementation) is documented, intended
152
+ behavior — `stable-id.js` deliberately hashes on normalized code shape rather than file/line,
153
+ specifically so near-identical code keeps one id across refactors. Moot here regardless, since
154
+ fix 2 above means neither finding fires again.
155
+
156
+ Not fixed, flagged rather than guessed at: the report's "uncertainty inversion" observation,
157
+ where an unproven, low-confidence finding still produced a confident PoC, ATT&CK mapping and
158
+ dollar-impact narrative downstream. `mitigation-composite.js`'s `exposed-in-prod` verdict is
159
+ confirmed to be answering an orthogonal question by design (would a known production control
160
+ block this, defaulting to exposed absent one) and never reads `proof.verdict` or confidence at
161
+ all — so the actual gap, if real, is in a PoC or dollar-estimate consumer not gating on proof
162
+ state, which needs its own dedicated investigation rather than a fix rushed into this release.
13
163
 
14
164
  ## 0.148.0 - NIST SP 800-171 Rev. 3 (CUI / CMMC basis) as the 10th bundled framework
15
165
 
@@ -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,62 @@ 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 [topReason, topCount] = ranked[0];
90
+ const allSameReason = ranked.length === 1;
91
+ const base = `strict mode requires complete finding provenance; ${badProvenance.length}/${totalFindings} finding(s) have status outside [complete, uncommitted]`;
92
+
93
+ if (topReason === 'not a Git repository' || topReason === 'repository state unavailable') {
94
+ return `${base} — reason: ${allSameReason ? 'all of them are' : `${topCount} of them are`} "${topReason}". ` +
95
+ `strict mode resolves finding provenance from git history, so it requires a real git repository ` +
96
+ `(a GitHub "Download ZIP" extracts without one). Run \`git init && git add -A && git commit -m init\` in ` +
97
+ `the scanned directory, point the scan at a real \`git clone\`, or drop --assurance strict for standard/advisory.`;
98
+ }
99
+ // engine.js's own comment on this branch: "unpinned_dep / no_lockfile and
100
+ // friends... describe the ABSENCE of a declaration, so 'which commit
101
+ // introduced this version' is not a question that has an answer to defer
102
+ // ... this is a known, disclosed limitation, not a bug... strict mode
103
+ // WILL fail on nearly any real project that has a package.json." That
104
+ // disclosure lived only in a source comment nobody hits this error reads —
105
+ // the README's own quickstart explicitly invites pointing --assurance
106
+ // strict at "your own project," where this is the single most likely
107
+ // outcome. Named here so the person who hits it learns it is expected and
108
+ // permanent, not something to keep investigating.
109
+ const supplyChainCount = ranked.filter(([r]) => r.startsWith('origin resolution does not apply to a')).reduce((s, [, n]) => s + n, 0);
110
+ if (supplyChainCount > 0 && supplyChainCount >= badProvenance.length / 2) {
111
+ return `${base} — ${supplyChainCount} of them describe an ABSENT dependency declaration ` +
112
+ `(an unpinned version, a missing lockfile) that has no origin commit to resolve, by design. This is a ` +
113
+ `known, permanent limitation: strict mode cannot pass while any are present, on any real project with ` +
114
+ `such a dependency. Fix the underlying SCA finding(s) (pin the version / add a lockfile) if you want ` +
115
+ `strict to pass, or use --assurance standard/advisory for a project you don't control the dependencies of.`;
116
+ }
117
+ if (allSameReason) {
118
+ return `${base} — all ${badProvenance.length} share the same reason: "${topReason}".`;
119
+ }
120
+ const breakdown = ranked.slice(0, 5).map(([reason, n]) => `${n}× "${reason}"`).join(', ');
121
+ return `${base} — breakdown: ${breakdown}${ranked.length > 5 ? ', …' : ''}.`;
122
+ }
123
+
68
124
  /**
69
125
  * @param {string} mode - one of ASSURANCE_MODES; invalid/missing degrades to the default.
70
126
  * @param {object|null} scanHealth - the engine's computed scan.scanHealth (FR-206).
@@ -157,7 +213,7 @@ function evaluateAssuranceMode(mode, scanHealth, findings = []) {
157
213
  return {
158
214
  ok: false,
159
215
  mode: 'strict',
160
- reason: `strict mode requires complete finding provenance; ${badProvenance.length} finding(s) have status outside [complete, uncommitted]`,
216
+ reason: _provenanceFailureReason(badProvenance, findings.length),
161
217
  conditions,
162
218
  };
163
219
  }
@@ -165,7 +221,7 @@ function evaluateAssuranceMode(mode, scanHealth, findings = []) {
165
221
  return { ok: true, mode: 'strict', reason: null, conditions };
166
222
  }
167
223
 
168
- const _internals = { _isValidMode };
224
+ const _internals = { _isValidMode, _provenanceFailureReason };
169
225
 
170
226
 
171
227
  /***/ })