@clear-capabilities/agentic-security-scanner 0.148.4 → 0.149.0

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,65 @@
10
10
 
11
11
 
12
12
 
13
+ ## 0.149.0 - Live progress reporting for long-running scans
14
+
15
+ A scan running deep interprocedural taint analysis or the Data Flow Explorer's lineage graph
16
+ build could sit silent for minutes with no sign anything was happening, which read as a hang and
17
+ was a real reason people quit the run before it finished. This release wires progress reporting
18
+ through the phases that were previously silent, and threads the existing stderr status line into
19
+ every command that runs a fresh scan.
20
+
21
+ 1. `runTaintEngine` (`src/dataflow/engine.js`) now reports live `current/total` progress from its
22
+ dominant per-function analysis loop via an optional `opts.onProgress`, additive and byte-
23
+ identical when omitted.
24
+ 2. `runFieldIdentityAnalysis` (`src/lineage/driver.js`) reports the same live per-function progress
25
+ for the lineage graph build, threaded through `graph-builder.js` -> `coverage.js` ->
26
+ `index.js`'s `buildLineageGraph`.
27
+ 3. Both the deep-taint and lineage-graph phases are one synchronous, unbreakable call each (Node's
28
+ event loop can't tick mid-call), so a "starting (budget Ns)" message prints the instant each
29
+ phase begins, then live progress once its main loop runs.
30
+ 4. The ~52 posture/provenance annotators (previously silent as a block) now report
31
+ `[Annotating] N/52 <name>` as each one runs.
32
+ 5. The stderr `\r[phase] current/total` status line, previously wired only into the default `scan`
33
+ command, is now shared (`scanProgressReporter()`/`clearScanProgressLine()`) and used by
34
+ `scan --watch`, `ci`, `verify-attestation`, and `dataflow watch` (the seed scan and every
35
+ rescan) too.
36
+
37
+ `posture`/`compliance`/`supply`/`triage`/`labs` read the persisted `last-scan.json` rather than
38
+ scanning themselves, so they inherit this the moment a `scan` produced that file; `org-scan`
39
+ (concurrent multi-repo) is deliberately left silent, since a shared progress line across parallel
40
+ workers would just interleave and garble.
41
+
42
+ Also: `ide/vscode`'s transitive `js-yaml` dev dependency (pulled in via `@vscode/vsce`) is bumped
43
+ 4.3.1 -> 4.3.2, closing a high-severity CPU-exhaustion advisory the release gate's dependency-
44
+ currency check found (GHSA-2883-xcg3-v3hh) — a non-breaking patch release, `npm audit fix` only.
45
+
46
+ ## 0.148.5 - Third premortem pass on the --assurance strict fix: clean bill of health, four polish items closed anyway
47
+
48
+ 0.148.4's fix was put through a THIRD adversarial premortem pass to check whether it introduced
49
+ anything new. It didn't — no High-or-above defect, and one specific hypothesis (a stale
50
+ pre-0.148.4 cached scan producing the old, wrong message after upgrading) was investigated and
51
+ disproven with direct evidence: `ci` always re-scans fresh, and the affected finding types never
52
+ route through the disk-cached resolver path at all. Four Low-severity polish items surfaced
53
+ anyway and are closed here, none of them changes to already-correct behavior:
54
+
55
+ 1. `cdn_no_integrity`/`dynamic_require` findings, alone with no other reason present, used to fall
56
+ through to a generic "share the same reason" message quoting the full raw internal string —
57
+ honest, but verbose, and missing the "(or drop to --assurance standard/advisory)" next step
58
+ every other named bucket already has. They now get their own short, specific message with that
59
+ same next step.
60
+ 2. The "MULTIPLE distinct reasons" message (fired when more than one category is present at once)
61
+ now renders as a bulleted, newline-separated list instead of one semicolon-joined paragraph —
62
+ each bullet is an independently actionable problem, and the old format buried that.
63
+ 3. `dynamic_require` and a standalone `no_lockfile` (previously only tested paired with
64
+ `unpinned_dep`) now each have their own direct test, closing a "shares a code path, never
65
+ independently verified" gap.
66
+ 4. A new completeness guard (`scanner/test/supply-chain-provenance-completeness.test.js`) asserts
67
+ every supply-chain finding type `engine.js` produces is deliberately classified as either a
68
+ genuine absence (no origin commit exists) or a real, resolvable-in-principle source location —
69
+ so a future finding type added to one detection loop and forgotten in the classification can't
70
+ silently tell a user a permanent limitation is fixable, or vice versa.
71
+
13
72
  ## 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
73
 
15
74
  0.148.2's fix for a confusing `--assurance strict` failure was itself put through an adversarial
@@ -53,6 +53,26 @@ const MACHINE_FORMATS = new Set([
53
53
  'json', 'sarif', 'oscal', 'cyclonedx', 'sbom', 'spdx', 'vex', 'openvex', 'pbom', 'aibom',
54
54
  ]);
55
55
  function isMachineFormat(fmt) { return MACHINE_FORMATS.has(String(fmt)); }
56
+
57
+ // Shared stderr progress reporter for any command that runs a fresh scan —
58
+ // the same `\r[phase] current/total file` status line the default `scan`
59
+ // command has always printed, extracted so every OTHER scan-driving command
60
+ // (`scan --watch`, `ci`, `verify-attestation`, `dataflow watch`) reports
61
+ // progress too instead of running silently. Gated on `stderr.isTTY` exactly
62
+ // like the original: piping to a file/CI log never gets a `\r`-spammed
63
+ // status line. `runFullScan` (engine.js) now reports progress for every
64
+ // phase, including the previously-silent deep-analysis/lineage/annotator
65
+ // phases — see that file's own `setProgress` call sites.
66
+ function scanProgressReporter() {
67
+ return (p) => {
68
+ if (process.stderr.isTTY) process.stderr.write(`\r[${p.phase}] ${p.current}/${p.total} ${p.file} `);
69
+ };
70
+ }
71
+ // Wipes the progress line printed by scanProgressReporter() above — call
72
+ // once a scan this reporter was attached to has finished.
73
+ function clearScanProgressLine() {
74
+ if (process.stderr.isTTY) process.stderr.write('\r' + ' '.repeat(80) + '\r');
75
+ }
56
76
  import { toCycloneDX, toSPDX } from '../src/posture/sbom.js';
57
77
  import { toPBOM } from '../src/sast/pipeline.js';
58
78
  import { buildAIBOM, aibomToMarkdown } from '../src/posture/aibom.js';
@@ -619,11 +639,13 @@ async function cmdScan(args) {
619
639
  process.env.AGENTIC_SECURITY_INCREMENTAL = '1';
620
640
  const { watchProject, computeDelta, persistStatus, renderStatusLine } = await import('../src/posture/watch-mode.js');
621
641
  process.stderr.write(`[watch] scanning ${targetAbs} on change — Ctrl-C to stop. Status → .agentic-security/watch-status.md\n`);
622
- const seed = await runScan(targetAbs, {});
642
+ const seed = await runScan(targetAbs, { onProgress: scanProgressReporter() });
643
+ clearScanProgressLine();
623
644
  let prevFindings = seed.scan.findings || [];
624
645
  await watchProject(targetAbs, async () => {
625
646
  try {
626
- const { scan } = await runScan(targetAbs, {});
647
+ const { scan } = await runScan(targetAbs, { onProgress: scanProgressReporter() });
648
+ clearScanProgressLine();
627
649
  const curr = scan.findings || [];
628
650
  const delta = computeDelta(prevFindings, curr);
629
651
  persistStatus(targetAbs, delta);
@@ -690,9 +712,7 @@ async function cmdScan(args) {
690
712
 
691
713
  const { scan, meta } = await runScan(target, {
692
714
  changedSince,
693
- onProgress: (p) => {
694
- if (process.stderr.isTTY) process.stderr.write(`\r[${p.phase}] ${p.current}/${p.total} ${p.file} `);
695
- },
715
+ onProgress: scanProgressReporter(),
696
716
  });
697
717
  // --require-provenance: flag (never fail) any finding whose provenance
698
718
  // isn't resolved, via scanHealth — deliberately independent of the
@@ -740,7 +760,7 @@ async function cmdScan(args) {
740
760
  // The BOM/attestation emitters stamp the producing engine's version into
741
761
  // their metadata; carry the real package version so it can never drift.
742
762
  if (meta && meta.engineVersion == null) meta.engineVersion = PKG_VERSION;
743
- if (process.stderr.isTTY) process.stderr.write('\r' + ' '.repeat(80) + '\r');
763
+ clearScanProgressLine();
744
764
 
745
765
  const only = args.flags.only;
746
766
  if (only) {
@@ -1230,7 +1250,8 @@ async function cmdCi(args) {
1230
1250
  else process.stderr.write(`[ci] full scan (no baseline ref detected)\n`);
1231
1251
 
1232
1252
  const profile = loadPersonaProfile(targetAbs, args);
1233
- const { scan, meta } = await runScan(target, { changedSince: baseline || null });
1253
+ const { scan, meta } = await runScan(target, { changedSince: baseline || null, onProgress: scanProgressReporter() });
1254
+ clearScanProgressLine();
1234
1255
 
1235
1256
  // Apply suppressions + overrides + packs, mirroring cmdScan's pipeline.
1236
1257
  scan.findings = applySuppressions(scan.findings || [], targetAbs, profile);
@@ -2740,7 +2761,8 @@ async function cmdVerifyRunAttestation(attestation, args) {
2740
2761
  const { normalizeFindings } = await import('../src/report/index.js');
2741
2762
  const { effectiveVersion } = await import('../src/posture/ruleset-version.js');
2742
2763
  const { verifyRunAttestation } = await import('../src/posture/attestation.js');
2743
- const { scan } = await runScan(projectPath);
2764
+ const { scan } = await runScan(projectPath, { onProgress: scanProgressReporter() });
2765
+ clearScanProgressLine();
2744
2766
  const r = verifyRunAttestation(attestation, {
2745
2767
  findings: normalizeFindings(scan),
2746
2768
  engineVersion: PKG_VERSION,
@@ -6224,7 +6246,8 @@ async function cmdDataflowWatch(args) {
6224
6246
  // uncached provenance-resolution cost on every edit), while every OTHER
6225
6247
  // state write named above stays fully suppressed. Do not "simplify" this
6226
6248
  // back to a bare runScan call.
6227
- const seed = await withStateWritesDisabled(() => runScan(targetAbs, {}), { exceptCategories: ['provenance-cache'] });
6249
+ const seed = await withStateWritesDisabled(() => runScan(targetAbs, { onProgress: scanProgressReporter() }), { exceptCategories: ['provenance-cache'] });
6250
+ clearScanProgressLine();
6228
6251
  if (!seed.scan.lineageGraph) {
6229
6252
  process.stderr.write(`agentic-security dataflow watch: seed scan produced no data-flow graph (${_lineageStatusReason(seed.scan.lineageStatus)}) — nothing to watch/diff against.\n`);
6230
6253
  return 1;
@@ -6266,7 +6289,8 @@ async function cmdDataflowWatch(args) {
6266
6289
  return;
6267
6290
  }
6268
6291
  try {
6269
- const { scan } = await withStateWritesDisabled(() => runScan(targetAbs, {}), { exceptCategories: ['provenance-cache'] });
6292
+ const { scan } = await withStateWritesDisabled(() => runScan(targetAbs, { onProgress: scanProgressReporter() }), { exceptCategories: ['provenance-cache'] });
6293
+ clearScanProgressLine();
6270
6294
  if (!scan.lineageGraph) {
6271
6295
  process.stderr.write(`[watch-dataflow] rescan produced no data-flow graph (${_lineageStatusReason(scan.lineageStatus)}) — skipping this change.\n`);
6272
6296
  return;
@@ -109,10 +109,19 @@ function _provenanceFailureReason(badProvenance, totalFindings) {
109
109
  // gap was an unfixable, by-design limitation).
110
110
  const supplyChainReasons = ranked.filter(([r]) => r.startsWith('origin resolution does not apply to a'));
111
111
  const supplyChainCount = supplyChainReasons.reduce((s, [, n]) => s + n, 0);
112
- const knownReasonSet = new Set([...gitReasons, ...supplyChainReasons].map(([r]) => r));
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));
113
122
  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);
123
+ const otherCount = badProvenance.length - gitCount - supplyChainCount - notYetWiredCount;
124
+ const knownCategoryCount = (gitCount > 0 ? 1 : 0) + (supplyChainCount > 0 ? 1 : 0) + (notYetWiredCount > 0 ? 1 : 0);
116
125
 
117
126
  // Exactly one KNOWN category, and nothing outside it — the shape every
118
127
  // caller before this fix assumed was the only shape, and the one every
@@ -133,11 +142,17 @@ function _provenanceFailureReason(badProvenance, totalFindings) {
133
142
  `(a GitHub "Download ZIP" extracts without one). Run \`git init && git add -A && git commit -m init\` in ` +
134
143
  `the scanned directory, point the scan at a real \`git clone\`, or drop --assurance strict for standard/advisory.`;
135
144
  }
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.`;
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.`;
141
156
  }
142
157
 
143
158
  // Two or more independently-blocking categories on the SAME scan — the
@@ -147,28 +162,39 @@ function _provenanceFailureReason(badProvenance, totalFindings) {
147
162
  // rerun, and hit a second wall the first run already had full information
148
163
  // about but never mentioned — the same "the tool knew and didn't tell me"
149
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.
150
171
  const segments = [];
151
172
  if (gitCount > 0) {
152
173
  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\`)`);
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\`.`);
155
176
  }
156
177
  if (supplyChainCount > 0) {
157
178
  segments.push(`${supplyChainCount} of them describe an ABSENT dependency declaration (unpinned version / ` +
158
179
  `missing lockfile) with no origin commit to resolve — a known, permanent limitation, not something a ` +
159
- `rerun will fix`);
180
+ `rerun will fix.`);
181
+ }
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.`);
160
185
  }
161
186
  if (otherCount > 0) {
162
187
  if (otherReasons.length === 1) {
163
- segments.push(`${otherCount} share the reason "${otherReasons[0][0]}"`);
188
+ segments.push(`${otherCount} share the reason "${otherReasons[0][0]}".`);
164
189
  } else {
165
190
  const breakdown = otherReasons.slice(0, 5).map(([reason, n]) => `${n}× "${reason}"`).join(', ');
166
- segments.push(`${otherCount} break down as: ${breakdown}${otherReasons.length > 5 ? ', …' : ''}`);
191
+ segments.push(`${otherCount} break down as: ${breakdown}${otherReasons.length > 5 ? ', …' : ''}.`);
167
192
  }
168
193
  }
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.`;
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.`;
172
198
  }
173
199
 
174
200
  /**