@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.
@@ -1 +1 @@
1
- 50ac012aad784a997fa9c835b7f9c38c1f482592eb6e2876e96e488d6e46f4f4 agentic-security.mjs
1
+ 9d52cc907c314d1ac60bc62b0c15a0ce4bbf28bdaeaa54a2e57782b9a75f97eb agentic-security.mjs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clear-capabilities/agentic-security-scanner",
3
- "version": "0.148.3",
3
+ "version": "0.148.4",
4
4
  "description": "Scanner engine for the agentic-security Claude Code plugin — SAST, SCA (function-level reachability + CISA KEV), secrets, IaC, prompt-injection, MCP/agent-tool audit, auth/authZ deep analysis, attack chains, PoC generation, business logic, toxic-combinations scoring, SBOM, pipeline integrity, compliance attestation, and more.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/engine.js CHANGED
@@ -10625,27 +10625,45 @@ function _deterministicFileTimings(timings) {
10625
10625
  // The supply-chain half. report/index.js normalizes EVERY supplyChain entry
10626
10626
  // into an SCA finding — not just the direct vulnerable_dep ones the resolver
10627
10627
  // can speak to — and pipeline/finding-schema.js requires findingProvenance on
10628
- // every channel. Three distinct populations reach this loop and they are not
10629
- // the same statement, so they do not share a limitation string:
10628
+ // every channel. FOUR distinct populations reach this loop, not three, and
10629
+ // they do not share a limitation string (adversarial premortem R2,
10630
+ // 2026-09-07, follow-up to the assurance-strict message fix — this split
10631
+ // used to be binary, and pipeline/assurance-mode.js's message building
10632
+ // depends on the exact wording below to tell them apart):
10630
10633
  //
10631
10634
  // - transitive vulnerable_deps: resolved by resolveTransitiveSCAOrigin
10632
10635
  // above (M3 §3.2) — this branch is now reached only if that annotation
10633
10636
  // pass itself failed to stamp the entry.
10634
- // - unpinned_dep / no_lockfile and friends: these describe the ABSENCE of a
10635
- // declaration, so "which commit introduced this version" is not a question
10636
- // that has an answer to defer.
10637
- // - anything the annotator failed to reach, as above.
10637
+ // - unpinned_dep / no_lockfile: these describe the ABSENCE of a
10638
+ // declaration, so "which commit introduced this version" is not a
10639
+ // question that has an answer to defer — permanently unresolvable, by
10640
+ // construction, not merely unresolved today.
10641
+ // - cdn_no_integrity / dynamic_require: NOT the same claim as the one
10642
+ // above, despite both reaching this loop as "not the vulnerable_dep
10643
+ // the resolver speaks to" — each of these carries a real `file`+`line`
10644
+ // (a specific `<script src>` tag or `require(...)` call someone
10645
+ // specifically wrote), so there IS a commit that introduced it; this
10646
+ // engine's resolver just isn't wired to walk it yet. Confusing this
10647
+ // with the absence case above told a user their finding was a
10648
+ // "permanent, by-design limitation" when it was really an ordinary,
10649
+ // fixable coverage gap.
10650
+ // - anything else: the annotator failed to reach it, as above.
10638
10651
  //
10639
- // The first two are honest `not_available` — that is exactly what the status
10640
- // is for. Only a genuine annotator failure is an `error`, which is why this
10641
- // loop distinguishes them rather than stamping one status for all three.
10652
+ // The first three are honest `not_available` — that is exactly what the
10653
+ // status is for. Only a genuine annotator failure is an `error`, which is
10654
+ // why this loop distinguishes them rather than stamping one status for all.
10655
+ const SUPPLY_CHAIN_ABSENCE_TYPES = new Set(['unpinned_dep', 'no_lockfile']);
10642
10656
  for (const sc of (supplyChain || [])) {
10643
10657
  if (!sc || typeof sc !== 'object' || sc.findingProvenance) continue;
10644
- sc.findingProvenance = emptyProvenance(PROVENANCE_STATUS.NOT_AVAILABLE, {
10645
- limitations: [sc.type === 'vulnerable_dep'
10646
- ? 'transitive dependency origin resolution failed for this entry (annotator error)'
10647
- : `origin resolution does not apply to a ${sc.type || 'non-vulnerability'} supply-chain entry`],
10648
- });
10658
+ let limitation;
10659
+ if (sc.type === 'vulnerable_dep') {
10660
+ limitation = 'transitive dependency origin resolution failed for this entry (annotator error)';
10661
+ } else if (SUPPLY_CHAIN_ABSENCE_TYPES.has(sc.type)) {
10662
+ limitation = `origin resolution does not apply to a ${sc.type} supply-chain entry`;
10663
+ } else {
10664
+ limitation = `origin resolution is not yet wired for a ${sc.type || 'non-vulnerability'} supply-chain entry (this describes a real source location, not an absent declaration — resolvable in principle, just not implemented today)`;
10665
+ }
10666
+ sc.findingProvenance = emptyProvenance(PROVENANCE_STATUS.NOT_AVAILABLE, { limitations: [limitation] });
10649
10667
  }
10650
10668
  // The OTHER two channels report/index.js normalizes into findings —
10651
10669
  // `scan.secrets` and `scan.logicVulns`. The same argument that produced the
@@ -72,39 +72,89 @@ function _provenanceFailureReason(badProvenance, totalFindings) {
72
72
  counts.set(reason, (counts.get(reason) || 0) + 1);
73
73
  }
74
74
  const ranked = [...counts.entries()].sort((a, b) => b[1] - a[1]);
75
- const [topReason, topCount] = ranked[0];
76
- const allSameReason = ranked.length === 1;
77
75
  const base = `strict mode requires complete finding provenance; ${badProvenance.length}/${totalFindings} finding(s) have status outside [complete, uncommitted]`;
78
76
 
79
- if (topReason === 'not a Git repository' || topReason === 'repository state unavailable') {
80
- return `${base} reason: ${allSameReason ? 'all of them are' : `${topCount} of them are`} "${topReason}". ` +
81
- `strict mode resolves finding provenance from git history, so it requires a real git repository ` +
82
- `(a GitHub "Download ZIP" extracts without one). Run \`git init && git add -A && git commit -m init\` in ` +
83
- `the scanned directory, point the scan at a real \`git clone\`, or drop --assurance strict for standard/advisory.`;
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 ? ', …' : ''}.`;
84
113
  }
85
- // engine.js's own comment on this branch: "unpinned_dep / no_lockfile and
86
- // friends... describe the ABSENCE of a declaration, so 'which commit
87
- // introduced this version' is not a question that has an answer to defer
88
- // ... this is a known, disclosed limitation, not a bug... strict mode
89
- // WILL fail on nearly any real project that has a package.json." That
90
- // disclosure lived only in a source comment nobody hits this error reads
91
- // the README's own quickstart explicitly invites pointing --assurance
92
- // strict at "your own project," where this is the single most likely
93
- // outcome. Named here so the person who hits it learns it is expected and
94
- // permanent, not something to keep investigating.
95
- const supplyChainCount = ranked.filter(([r]) => r.startsWith('origin resolution does not apply to a')).reduce((s, [, n]) => s + n, 0);
96
- if (supplyChainCount > 0 && supplyChainCount >= badProvenance.length / 2) {
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
+ }
97
122
  return `${base} — ${supplyChainCount} of them describe an ABSENT dependency declaration ` +
98
123
  `(an unpinned version, a missing lockfile) that has no origin commit to resolve, by design. This is a ` +
99
124
  `known, permanent limitation: strict mode cannot pass while any are present, on any real project with ` +
100
125
  `such a dependency. Fix the underlying SCA finding(s) (pin the version / add a lockfile) if you want ` +
101
126
  `strict to pass, or use --assurance standard/advisory for a project you don't control the dependencies of.`;
102
127
  }
103
- if (allSameReason) {
104
- return `${base} all ${badProvenance.length} share the same reason: "${topReason}".`;
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
+ }
105
154
  }
106
- const breakdown = ranked.slice(0, 5).map(([reason, n]) => `${n}× "${reason}"`).join(', ');
107
- return `${base} breakdown: ${breakdown}${ranked.length > 5 ? ', …' : ''}.`;
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.`;
108
158
  }
109
159
 
110
160
  /**