@clear-capabilities/agentic-security-scanner 0.148.3 → 0.148.5
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 +61 -0
- package/dist/4970.index.js +104 -28
- package/dist/agentic-security.mjs +1 -1
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +2 -2
- package/src/engine.js +46 -14
- package/src/pipeline/assurance-mode.js +104 -28
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,67 @@
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
## 0.148.5 - Third premortem pass on the --assurance strict fix: clean bill of health, four polish items closed anyway
|
|
14
|
+
|
|
15
|
+
0.148.4's fix was put through a THIRD adversarial premortem pass to check whether it introduced
|
|
16
|
+
anything new. It didn't — no High-or-above defect, and one specific hypothesis (a stale
|
|
17
|
+
pre-0.148.4 cached scan producing the old, wrong message after upgrading) was investigated and
|
|
18
|
+
disproven with direct evidence: `ci` always re-scans fresh, and the affected finding types never
|
|
19
|
+
route through the disk-cached resolver path at all. Four Low-severity polish items surfaced
|
|
20
|
+
anyway and are closed here, none of them changes to already-correct behavior:
|
|
21
|
+
|
|
22
|
+
1. `cdn_no_integrity`/`dynamic_require` findings, alone with no other reason present, used to fall
|
|
23
|
+
through to a generic "share the same reason" message quoting the full raw internal string —
|
|
24
|
+
honest, but verbose, and missing the "(or drop to --assurance standard/advisory)" next step
|
|
25
|
+
every other named bucket already has. They now get their own short, specific message with that
|
|
26
|
+
same next step.
|
|
27
|
+
2. The "MULTIPLE distinct reasons" message (fired when more than one category is present at once)
|
|
28
|
+
now renders as a bulleted, newline-separated list instead of one semicolon-joined paragraph —
|
|
29
|
+
each bullet is an independently actionable problem, and the old format buried that.
|
|
30
|
+
3. `dynamic_require` and a standalone `no_lockfile` (previously only tested paired with
|
|
31
|
+
`unpinned_dep`) now each have their own direct test, closing a "shares a code path, never
|
|
32
|
+
independently verified" gap.
|
|
33
|
+
4. A new completeness guard (`scanner/test/supply-chain-provenance-completeness.test.js`) asserts
|
|
34
|
+
every supply-chain finding type `engine.js` produces is deliberately classified as either a
|
|
35
|
+
genuine absence (no origin commit exists) or a real, resolvable-in-principle source location —
|
|
36
|
+
so a future finding type added to one detection loop and forgotten in the classification can't
|
|
37
|
+
silently tell a user a permanent limitation is fixable, or vice versa.
|
|
38
|
+
|
|
39
|
+
## 0.148.4 - Adversarial premortem re-run on the 0.148.2/0.148.3 --assurance strict fix: two real defects found and fixed
|
|
40
|
+
|
|
41
|
+
0.148.2's fix for a confusing `--assurance strict` failure was itself put through an adversarial
|
|
42
|
+
premortem rather than trusted on its own say-so, the same discipline applied to the compliance-
|
|
43
|
+
framework work earlier. It found the message-building logic held up in the common single-reason
|
|
44
|
+
case, but had two real defects when a scan had more than one kind of provenance problem at once —
|
|
45
|
+
which, on a real project, is not an edge case.
|
|
46
|
+
|
|
47
|
+
1. **Multiple concurrent reasons silently collapsed to one.** A non-git directory with an
|
|
48
|
+
unpinned dependency produces BOTH a `'not a Git repository'` reason (every SAST/secrets/logic
|
|
49
|
+
finding) AND a `'origin resolution does not apply to...'` reason (the unpinned-dependency SCA
|
|
50
|
+
finding) on the very same scan — not hypothetically; `engine.js`'s two SCA populations that get
|
|
51
|
+
real git-history resolution are filtered to `type === 'vulnerable_dep'` only, so `unpinned_dep`/
|
|
52
|
+
`no_lockfile` never pass through the git-repo check at all. The message-building code picked
|
|
53
|
+
whichever reason had the most findings and silently dropped the other, so a user could fix the
|
|
54
|
+
reported problem (initialize git), rerun, and hit a second wall the tool had full information
|
|
55
|
+
about on the very first run but never mentioned — a milder recurrence of the exact "the tool
|
|
56
|
+
knew and didn't tell me" complaint the original fix existed to close. `_provenanceFailureReason`
|
|
57
|
+
(`scanner/src/pipeline/assurance-mode.js`) now reports every real category present, not just the
|
|
58
|
+
largest one.
|
|
59
|
+
|
|
60
|
+
2. **Two structurally different SCA gaps were given the same, wrong advice.** `unpinned_dep`/
|
|
61
|
+
`no_lockfile` genuinely have no origin commit to resolve (they describe an absent declaration) —
|
|
62
|
+
correctly labeled a permanent, by-design limitation. But `cdn_no_integrity`/`dynamic_require`
|
|
63
|
+
both carry a real file:line (a specific `<script src>` tag or `require(...)` call someone wrote)
|
|
64
|
+
that a future resolver update genuinely could walk; `engine.js`'s provenance-stamping loop
|
|
65
|
+
previously gave all four types the identical limitation string, so the message-building code told
|
|
66
|
+
a user with a `cdn_no_integrity` finding to stop investigating a resolvable coverage gap because
|
|
67
|
+
it looked identical to a truly unresolvable one. `engine.js` now gives the two classes distinct,
|
|
68
|
+
honest limitation strings.
|
|
69
|
+
|
|
70
|
+
Both landed with new regression tests (`scanner/test/assurance-mode.test.js`) covering the exact
|
|
71
|
+
multi-reason interaction that exposed the first defect, and the cdn/dynamic-require case for the
|
|
72
|
+
second. No behavior changed for the single-reason case most scans will actually hit.
|
|
73
|
+
|
|
13
74
|
## 0.148.3 - Dependency currency fix; supersedes 0.148.2, which never published
|
|
14
75
|
|
|
15
76
|
The `v0.148.2` tag was pushed but its release workflow's dependency-currency gate failed on a
|
package/dist/4970.index.js
CHANGED
|
@@ -86,39 +86,115 @@ 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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
+
// Third known bucket (S1, adversarial premortem third pass, 2026-09-07):
|
|
113
|
+
// cdn_no_integrity/dynamic_require's "not yet wired" string (see the
|
|
114
|
+
// engine.js comment this file's `supplyChainReasons` block already
|
|
115
|
+
// references) was previously falling through to the generic `otherReasons`
|
|
116
|
+
// path below — honest, but verbose, and with no recommended next step,
|
|
117
|
+
// unlike every other named bucket here. Giving it its own bucket closes
|
|
118
|
+
// that inconsistency without touching the two already-fixed buckets.
|
|
119
|
+
const notYetWiredReasons = ranked.filter(([r]) => r.startsWith('origin resolution is not yet wired for a'));
|
|
120
|
+
const notYetWiredCount = notYetWiredReasons.reduce((s, [, n]) => s + n, 0);
|
|
121
|
+
const knownReasonSet = new Set([...gitReasons, ...supplyChainReasons, ...notYetWiredReasons].map(([r]) => r));
|
|
122
|
+
const otherReasons = ranked.filter(([r]) => !knownReasonSet.has(r));
|
|
123
|
+
const otherCount = badProvenance.length - gitCount - supplyChainCount - notYetWiredCount;
|
|
124
|
+
const knownCategoryCount = (gitCount > 0 ? 1 : 0) + (supplyChainCount > 0 ? 1 : 0) + (notYetWiredCount > 0 ? 1 : 0);
|
|
125
|
+
|
|
126
|
+
// Exactly one KNOWN category, and nothing outside it — the shape every
|
|
127
|
+
// caller before this fix assumed was the only shape, and the one every
|
|
128
|
+
// existing test was written against. Kept as tight, single-topic prose
|
|
129
|
+
// rather than the multi-segment form below.
|
|
130
|
+
if (knownCategoryCount === 0) {
|
|
131
|
+
if (otherReasons.length === 1) {
|
|
132
|
+
return `${base} — all ${badProvenance.length} share the same reason: "${otherReasons[0][0]}".`;
|
|
133
|
+
}
|
|
134
|
+
const breakdown = otherReasons.slice(0, 5).map(([reason, n]) => `${n}× "${reason}"`).join(', ');
|
|
135
|
+
return `${base} — breakdown: ${breakdown}${otherReasons.length > 5 ? ', …' : ''}.`;
|
|
136
|
+
}
|
|
137
|
+
if (knownCategoryCount === 1 && otherCount === 0) {
|
|
138
|
+
if (gitCount > 0) {
|
|
139
|
+
const gitReasonNames = gitReasons.map(([r]) => `"${r}"`).join(' and ');
|
|
140
|
+
return `${base} — reason: ${gitCount === badProvenance.length ? 'all of them are' : `${gitCount} of them are`} ${gitReasonNames}. ` +
|
|
141
|
+
`strict mode resolves finding provenance from git history, so it requires a real git repository ` +
|
|
142
|
+
`(a GitHub "Download ZIP" extracts without one). Run \`git init && git add -A && git commit -m init\` in ` +
|
|
143
|
+
`the scanned directory, point the scan at a real \`git clone\`, or drop --assurance strict for standard/advisory.`;
|
|
144
|
+
}
|
|
145
|
+
if (supplyChainCount > 0) {
|
|
146
|
+
return `${base} — ${supplyChainCount} of them describe an ABSENT dependency declaration ` +
|
|
147
|
+
`(an unpinned version, a missing lockfile) that has no origin commit to resolve, by design. This is a ` +
|
|
148
|
+
`known, permanent limitation: strict mode cannot pass while any are present, on any real project with ` +
|
|
149
|
+
`such a dependency. Fix the underlying SCA finding(s) (pin the version / add a lockfile) if you want ` +
|
|
150
|
+
`strict to pass, or use --assurance standard/advisory for a project you don't control the dependencies of.`;
|
|
151
|
+
}
|
|
152
|
+
return `${base} — ${notYetWiredCount} of them point at a real source location (a CDN script tag, a dynamic ` +
|
|
153
|
+
`require) this engine can't yet trace back to the commit that introduced it — unlike the ABSENT-declaration ` +
|
|
154
|
+
`case above, this is an ordinary coverage gap, not a permanent limitation, but it isn't fixable from your ` +
|
|
155
|
+
`side either. Use --assurance standard/advisory if you need this scan to pass today.`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Two or more independently-blocking categories on the SAME scan — the
|
|
159
|
+
// defect this closes (adversarial premortem R1, 2026-09-07): the old
|
|
160
|
+
// code picked whichever category had the most findings and silently
|
|
161
|
+
// dropped every other one, so a user could "fix" the reported problem,
|
|
162
|
+
// rerun, and hit a second wall the first run already had full information
|
|
163
|
+
// about but never mentioned — the same "the tool knew and didn't tell me"
|
|
164
|
+
// complaint this whole function exists to fix, recurring in a milder form.
|
|
165
|
+
//
|
|
166
|
+
// Rendered as a bulleted, newline-separated list rather than one
|
|
167
|
+
// semicolon-joined paragraph (S2, adversarial premortem third pass,
|
|
168
|
+
// 2026-09-07) — each bullet is independently actionable, and a wall of
|
|
169
|
+
// clauses buried the fact that they are SEPARATE problems, each with its
|
|
170
|
+
// own fix, rather than one problem described three ways.
|
|
171
|
+
const segments = [];
|
|
172
|
+
if (gitCount > 0) {
|
|
173
|
+
const gitReasonNames = gitReasons.map(([r]) => `"${r}"`).join(' and ');
|
|
174
|
+
segments.push(`${gitCount} of them are ${gitReasonNames} — strict mode requires a real git repository; ` +
|
|
175
|
+
`run \`git init && git add -A && git commit\`, or scan a real \`git clone\`.`);
|
|
176
|
+
}
|
|
177
|
+
if (supplyChainCount > 0) {
|
|
178
|
+
segments.push(`${supplyChainCount} of them describe an ABSENT dependency declaration (unpinned version / ` +
|
|
179
|
+
`missing lockfile) with no origin commit to resolve — a known, permanent limitation, not something a ` +
|
|
180
|
+
`rerun will fix.`);
|
|
98
181
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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.`;
|
|
182
|
+
if (notYetWiredCount > 0) {
|
|
183
|
+
segments.push(`${notYetWiredCount} of them point at a real source location this engine can't yet trace ` +
|
|
184
|
+
`back to a commit — an ordinary coverage gap, not a permanent limitation, but not fixable from your side.`);
|
|
116
185
|
}
|
|
117
|
-
if (
|
|
118
|
-
|
|
186
|
+
if (otherCount > 0) {
|
|
187
|
+
if (otherReasons.length === 1) {
|
|
188
|
+
segments.push(`${otherCount} share the reason "${otherReasons[0][0]}".`);
|
|
189
|
+
} else {
|
|
190
|
+
const breakdown = otherReasons.slice(0, 5).map(([reason, n]) => `${n}× "${reason}"`).join(', ');
|
|
191
|
+
segments.push(`${otherCount} break down as: ${breakdown}${otherReasons.length > 5 ? ', …' : ''}.`);
|
|
192
|
+
}
|
|
119
193
|
}
|
|
120
|
-
const
|
|
121
|
-
return `${base} —
|
|
194
|
+
const bullets = segments.map((s) => ` - ${s}`).join('\n');
|
|
195
|
+
return `${base} — MULTIPLE distinct reasons, not just one:\n${bullets}\nEvery category above must be ` +
|
|
196
|
+
`resolved for strict to pass (or drop to --assurance standard/advisory) — fixing only one will surface the ` +
|
|
197
|
+
`next on your following run.`;
|
|
122
198
|
}
|
|
123
199
|
|
|
124
200
|
/**
|