@clear-capabilities/agentic-security-scanner 0.148.3 → 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 CHANGED
@@ -10,6 +10,41 @@
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
+
13
48
  ## 0.148.3 - Dependency currency fix; supersedes 0.148.2, which never published
14
49
 
15
50
  The `v0.148.2` tag was pushed but its release workflow's dependency-currency gate failed on a
@@ -86,39 +86,89 @@ function _provenanceFailureReason(badProvenance, totalFindings) {
86
86
  counts.set(reason, (counts.get(reason) || 0) + 1);
87
87
  }
88
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
89
  const base = `strict mode requires complete finding provenance; ${badProvenance.length}/${totalFindings} finding(s) have status outside [complete, uncommitted]`;
92
90
 
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.`;
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 ? ', …' : ''}.`;
98
127
  }
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) {
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
+ }
111
136
  return `${base} — ${supplyChainCount} of them describe an ABSENT dependency declaration ` +
112
137
  `(an unpinned version, a missing lockfile) that has no origin commit to resolve, by design. This is a ` +
113
138
  `known, permanent limitation: strict mode cannot pass while any are present, on any real project with ` +
114
139
  `such a dependency. Fix the underlying SCA finding(s) (pin the version / add a lockfile) if you want ` +
115
140
  `strict to pass, or use --assurance standard/advisory for a project you don't control the dependencies of.`;
116
141
  }
117
- if (allSameReason) {
118
- return `${base} all ${badProvenance.length} share the same reason: "${topReason}".`;
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
+ }
119
168
  }
120
- const breakdown = ranked.slice(0, 5).map(([reason, n]) => `${n}× "${reason}"`).join(', ');
121
- return `${base} breakdown: ${breakdown}${ranked.length > 5 ? ', …' : ''}.`;
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.`;
122
172
  }
123
173
 
124
174
  /**