@clear-capabilities/agentic-security-scanner 0.184.0 → 0.185.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/bin/agentic-security.js +87 -0
  3. package/dist/{dad58d1113a12c23a142.json → 0b301d45b231f144a1b4.json} +26 -9
  4. package/dist/1128.index.js +908 -0
  5. package/dist/1336.index.js +261 -112
  6. package/dist/1665.index.js +828 -0
  7. package/dist/9579.index.js +1 -1
  8. package/dist/agentic-security.mjs +15 -15
  9. package/dist/agentic-security.mjs.sha256 +1 -1
  10. package/dist/frontend/src/shell.js +12 -2
  11. package/package.json +26 -9
  12. package/src/dataflow/CLAUDE.md +43 -1
  13. package/src/dataflow/backward.js +42 -7
  14. package/src/dataflow/cross-service-taint.js +83 -1
  15. package/src/dataflow/demand-query.js +929 -0
  16. package/src/dataflow/dep-fingerprint.js +718 -0
  17. package/src/dataflow/engine.js +48 -6
  18. package/src/dataflow/ifds-precise.js +22 -0
  19. package/src/dataflow/incremental-replay.js +118 -0
  20. package/src/dataflow/incremental-session.js +257 -0
  21. package/src/dataflow/incremental.js +237 -35
  22. package/src/dataflow/index.js +25 -94
  23. package/src/dataflow/summaries.js +72 -6
  24. package/src/dataflow/tabulation.js +80 -24
  25. package/src/engine.js +51 -3
  26. package/src/graph/CLAUDE.md +31 -0
  27. package/src/graph/adapters/artifact.js +81 -0
  28. package/src/graph/adapters/crossrepo.js +75 -0
  29. package/src/graph/adapters/dataflow.js +76 -0
  30. package/src/graph/adapters/provenance.js +92 -0
  31. package/src/graph/adapters/semantic.js +89 -0
  32. package/src/graph/crosswalk-builders.js +152 -0
  33. package/src/graph/crosswalk.js +146 -0
  34. package/src/graph/index.js +15 -0
  35. package/src/graph/observation.js +57 -0
  36. package/src/graph/refs.js +105 -0
  37. package/src/graph/registry.js +129 -0
  38. package/src/graph/relations.js +81 -0
  39. package/src/graph/scc.js +241 -0
  40. package/src/graph/view.js +254 -0
  41. package/src/lineage/CLAUDE.md +71 -8
  42. package/src/lineage/DESIGN_GRAPH_BUILDER.md +1 -1
  43. package/src/lineage/DESIGN_PATH_PROVENANCE.md +6 -0
  44. package/src/lineage/boundary-contract.js +273 -0
  45. package/src/lineage/completion.js +205 -0
  46. package/src/lineage/continuation.js +140 -0
  47. package/src/lineage/control-credit.js +313 -0
  48. package/src/lineage/correlation.js +199 -0
  49. package/src/lineage/coverage.js +8 -1
  50. package/src/lineage/cross-repo-link.js +22 -0
  51. package/src/lineage/cut-planner.js +548 -0
  52. package/src/lineage/driver.js +219 -66
  53. package/src/lineage/engine.js +166 -21
  54. package/src/lineage/export-json.js +5 -0
  55. package/src/lineage/graph-builder.js +84 -7
  56. package/src/lineage/haskell-view.js +59 -0
  57. package/src/lineage/index.js +3 -0
  58. package/src/lineage/nix-view.js +35 -0
  59. package/src/lineage/owned-build.js +77 -0
  60. package/src/lineage/owned-worker-entry.js +22 -0
  61. package/src/lineage/owned-worker.js +95 -0
  62. package/src/lineage/path-evidence.js +123 -0
  63. package/src/lineage/path-predicates.js +529 -0
  64. package/src/lineage/path-query.js +189 -20
  65. package/src/lineage/path-store.js +191 -17
  66. package/src/lineage/scc-solver.js +186 -0
  67. package/src/lineage/smt-orchestrator.js +138 -0
  68. package/src/lineage/source-seeding.js +36 -2
  69. package/src/lineage/summaries.js +66 -4
  70. package/src/lineage/value-versions.js +130 -0
  71. package/src/lineage/witness-selection.js +95 -0
  72. package/src/pipeline/scan-health.js +18 -0
  73. package/src/posture/CLAUDE.md +13 -0
  74. package/src/posture/evidence-bundle.js +1 -1
  75. package/src/posture/witness-bundle.js +573 -0
  76. package/src/posture/witness-runtime.js +276 -0
  77. package/src/report/index.js +2 -0
@@ -1,4 +1,7 @@
1
- import { emptyState } from './field-identity.js';
1
+ import { emptyState, hashState } from './field-identity.js';
2
+ import { resolveCorrelationVersion } from './correlation.js';
3
+ import { computeScc } from '../graph/scc.js';
4
+ import { solveRecursiveComponent } from './scc-solver.js';
2
5
  import { analyzeFunctionFieldIdentity } from './engine.js';
3
6
  import {
4
7
  FieldIdentitySummaryCache,
@@ -32,6 +35,27 @@ import {
32
35
  // entirely on B1's existing `_stack`-based bottom-stub for safety on a
33
36
  // recursive/cyclic call graph, exactly as B1-B3 already did.
34
37
  //
38
+ // DAG-01 UPDATE (SCC scheduling). Functions are now scheduled over the
39
+ // condensation of the call graph (`graph/scc.js`), callees before callers,
40
+ // ties broken by qid. Call edges come from `callGraph.edges` (caller ->
41
+ // callee qids, as `buildCallGraph` emits) or `opts.calleesOf(fn) -> qid[]`;
42
+ // a hand-built graph with neither has no edges, so every function is its own
43
+ // component and the order is exactly the old sorted-qid order. A component
44
+ // that is a single function with no self-call runs the legacy single-pass
45
+ // step unchanged. A RECURSIVE component (two or more functions, or a
46
+ // self-call) is solved by `scc-solver.js`: re-analyze each member until a
47
+ // full round changes no member summary (see that file for the bounded fact
48
+ // domain, the join/widening policy and what "converged" does and does not
49
+ // mean). Every analysis in a round re-reads the previous round's summaries.
50
+ // The return value gains `convergence`; `results` keeps ascending-qid order.
51
+ //
52
+ // Budgets (all optional, deterministic where possible): `opts.maxRounds`,
53
+ // `opts.maxPathDepth`, `opts.maxFunctionAnalyses` (a work count),
54
+ // `opts.deadlineMs` with `opts.now()` (default Date.now), `opts.signal`.
55
+ // When any of them stops the run, `convergence.converged` is false,
56
+ // `status` is 'partial', and `remainingFrontier` lists the qids that were
57
+ // not finished. A partial result must never be read as a safety proof.
58
+ //
35
59
  // `callGraph` must be a real object from
36
60
  // `scanner/src/ir/callgraph.js#buildCallGraph` (`{functions,
37
61
  // resolveKnownCallee, ...}`) or an equivalent hand-built fixture exposing
@@ -66,78 +90,207 @@ export function runFieldIdentityAnalysis(callGraph, opts = {}) {
66
90
  const fnList = callGraph && callGraph.functions
67
91
  ? [...callGraph.functions.values()].sort((a, b) => (a.qid < b.qid ? -1 : a.qid > b.qid ? 1 : 0))
68
92
  : [];
69
- // Live progress, additive/opt-in — mirrors dataflow/engine.js's own
70
- // `runTaintEngine` onProgress precedent exactly. Unlike that engine, this
71
- // driver has only ONE pass over `fnList` (no fixed-point pre-passes), so
72
- // every function this driver analyzes is reported, not just a dominant
73
- // subset.
93
+ // Live progress, additive/opt-in. Each function is reported once, the first
94
+ // time it is analyzed (recursive re-rounds are not re-reported).
74
95
  const onProgress = typeof opts.onProgress === 'function' ? opts.onProgress : null;
75
96
  let _progressN = 0;
97
+ const reported = new Set();
98
+ const progress = (fn) => {
99
+ if (!onProgress || reported.has(fn.qid)) return;
100
+ reported.add(fn.qid);
101
+ onProgress({ current: ++_progressN, total: fnList.length });
102
+ };
76
103
 
77
104
  const results = new Map();
78
- for (const fn of fnList) {
79
- if (onProgress) onProgress({ current: ++_progressN, total: fnList.length });
105
+ const fnByQid = new Map(fnList.map(f => [f.qid, f]));
106
+
107
+ // --- Schedule: SCC condensation of the call graph, callees first. -------
108
+ const edges = [];
109
+ const edgeSeen = new Set();
110
+ const addEdge = (caller, callee) => {
111
+ if (!fnByQid.has(caller) || !fnByQid.has(callee)) return;
112
+ const k = `${caller}\u0000${callee}`;
113
+ if (edgeSeen.has(k)) return;
114
+ edgeSeen.add(k);
115
+ // Dependency direction: the callee must come before the caller.
116
+ edges.push([callee, caller]);
117
+ };
118
+ if (callGraph && Array.isArray(callGraph.edges)) {
119
+ for (const e of callGraph.edges) if (e && typeof e.callee === 'string') addEdge(e.caller, e.callee);
120
+ }
121
+ if (typeof opts.calleesOf === 'function') {
122
+ for (const fn of fnList) for (const c of (opts.calleesOf(fn) || [])) addEdge(fn.qid, c);
123
+ }
124
+ const scc = computeScc({ nodes: fnList.map(f => f.qid), edges });
125
+
126
+ // --- Budget. ------------------------------------------------------------
127
+ const now = typeof opts.now === 'function' ? opts.now : Date.now;
128
+ let work = 0;
129
+ let stopReason = null;
130
+ const shouldStop = () => {
131
+ if (stopReason) return stopReason;
132
+ if (opts.signal && opts.signal.aborted) stopReason = 'aborted';
133
+ else if (typeof opts.deadlineMs === 'number' && now() >= opts.deadlineMs) stopReason = 'deadline';
134
+ else if (Number.isFinite(opts.deadline) && Date.now() > opts.deadline) stopReason = 'deadline';
135
+ else if (typeof opts.maxFunctionAnalyses === 'number' && work >= opts.maxFunctionAnalyses) stopReason = 'work-limit';
136
+ return stopReason;
137
+ };
138
+
139
+ // Sub-project E, increment 1 (DESIGN_GRAPH_BUILDER.md section 3): the
140
+ // seeding hook. `opts.seedEntryState(fn)` returns the entry state `fn` is
141
+ // analyzed under; a falsy return means "no seed". With no hook, this IS
142
+ // `emptyState()` and every output is byte-identical to the pre-hook driver.
143
+ const entryStateOf = (fn) => (opts.seedEntryState ? (opts.seedEntryState(fn) || emptyState()) : emptyState());
144
+ const ctxFor = (fn) => {
80
145
  const lookupCallee = createCallGraphLookup(callGraph, fn.file);
81
146
  const resolveCallSummary = createCallSummaryResolver(cache, lookupCallee);
82
- // Path provenance (Sub-project C, increment 3, §13.7 item 14): thread
83
- // opts.recordHop into the per-function ctx CONDITIONALLY, so a caller
84
- // that supplies none gets a byte-identical `{ resolveCallSummary }`
85
- // object to what this function constructed before this change —
86
- // Decision 7.2's "true by construction" property, extended to the
87
- // driver.
147
+ // Path provenance (Sub-project C, increment 3, section 13.7 item 14):
148
+ // thread opts.recordHop CONDITIONALLY so a caller that supplies none gets
149
+ // a byte-identical `{ resolveCallSummary }` object (Decision 7.2).
88
150
  const ctx = opts.recordHop ? { resolveCallSummary, recordHop: opts.recordHop } : { resolveCallSummary };
89
- // Sub-project E, increment 1 (DESIGN_GRAPH_BUILDER.md §3): the seeding
90
- // hook. `opts.seedEntryState(fn)` returns the entry state this function
91
- // should be analyzed under — a `field-identity.js` state carrying the
92
- // data-element identities a source registry matched at real call sites
93
- // INSIDE `fn`. Additive and opt-in, exactly like `opts.recordHop`
94
- // above: with no hook supplied, `entryState` is `emptyState()` and both
95
- // this call and the `cache.set` below are byte-identical to what this
96
- // loop did before the hook existed (Decision 7.2's "true by
97
- // construction" property, extended once more). A hook returning a
98
- // falsy value is treated as "no seed for this function", so a caller
99
- // can seed a subset without special-casing the rest.
100
- const entryState = opts.seedEntryState ? (opts.seedEntryState(fn) || emptyState()) : emptyState();
101
- const result = analyzeFunctionFieldIdentity(fn, entryState, ctx);
102
- results.set(fn.qid, result);
103
- // Seed the cache with this function's OWN empty-entry summary directly
104
- // (not via cache.compute, which is reserved for a CALL SITE resolving
105
- // an as-yet-uncomputed callee) — this is what lets a LATER function in
106
- // fnList that calls this one reuse the driver's own result instead of
107
- // silently recomputing it a second time.
108
- //
109
- // This CAN overwrite an entry a callee-triggered lazy compute() already
110
- // wrote for the SAME (qid, emptyState()) key earlier in this loop — and
111
- // a final whole-branch review proved by direct construction that the
112
- // two computations are NOT guaranteed to agree: createCallGraphLookup's
113
- // `callerFile` is fixed to the CALLING function's file, so when fn was
114
- // first analyzed lazily (as someone else's callee), any bare-identifier
115
- // call fn itself makes was resolved preferring THAT CALLER's file, not
116
- // fn's own. This loop's own direct call above uses fn.file, the
117
- // correct scope, so the overwrite here always uses the more precise of
118
- // the two answers, never a worse one — but it is a real overwrite of a
119
- // possibly-different prior value, not merely redundant re-computation
120
- // of an identical one. Today every entry state is emptyState() (no
121
- // source registry yet), so no identity can actually differ between the
122
- // two computations and this is unobservable; it becomes load-bearing
123
- // the moment entry states carry real identities.
124
- //
125
- // Sub-project E, increment 1 CLOSED THE SECOND, WORSE HALF of that
126
- // hazard, which the paragraph above did not name: the KEY. This line
127
- // used to write the summary under `emptyState()` unconditionally, i.e.
128
- // under a key that claims "this is what `fn` does when nothing flows
129
- // in". Once a seed puts real identities into `entryState`, that claim
130
- // is FALSE — a later call site resolving `fn` with clean arguments
131
- // builds an empty callee entry state, hits this key, and is handed
132
- // return facts that exist only because the DRIVER seeded them. That is
133
- // a fabricated identity at a call site nothing tainted, and it is
134
- // measurable (see DESIGN_GRAPH_BUILDER.md §3.6: a real two-function
135
- // fixture, plus a 27% swing in accepted hops on a 33-file project).
136
- // Keying by the state actually analyzed is both the fix and a no-op
137
- // when no hook is supplied — `entryState` IS `emptyState()` then, so
138
- // this line stays byte-identical for every pre-existing caller.
139
- cache.set(fn.qid, entryState, summaryFromAnalysisResult(result));
151
+ // DAG-03: correlation version 2 labels hops with derivation, value-version
152
+ // and invocation evidence. Off unless asked for; needs a recorder.
153
+ if (opts.recordHop && resolveCorrelationVersion(opts.correlationVersion) === 2) ctx.correlationVersion = 2;
154
+ // DAG-07: optional per-function iteration ceiling and cooperative stop
155
+ // (engine.js ignores a non-positive-integer ceiling, so this cannot loosen it).
156
+ if (opts.maxIterations !== undefined) ctx.maxIterations = opts.maxIterations;
157
+ if (opts.signal || opts.deadline !== undefined || opts.deadlineMs !== undefined) ctx.shouldStop = () => { const r = shouldStop(); return r === 'aborted' || r === 'deadline'; };
158
+ return ctx;
159
+ };
160
+ const notesFrom = cache.completionNotes().length;
161
+
162
+ const convergence = {
163
+ converged: true,
164
+ status: 'complete',
165
+ rounds: 0,
166
+ changedFacts: [],
167
+ changedFactsTotal: 0,
168
+ wideningReasons: [],
169
+ remainingFrontier: [],
170
+ stoppedBy: null,
171
+ components: [],
172
+ };
173
+ const wideningReasons = new Set();
174
+ const frontier = new Set();
175
+
176
+ const orderedComponents = scc.order.map(id => scc.components[id]);
177
+ for (const comp of orderedComponents) {
178
+ if (!comp.recursive) {
179
+ const fn = fnByQid.get(comp.members[0]);
180
+ if (shouldStop()) { frontier.add(fn.qid); continue; }
181
+ work++;
182
+ progress(fn);
183
+ const entryState = entryStateOf(fn);
184
+ const result = analyzeFunctionFieldIdentity(fn, entryState, ctxFor(fn));
185
+ results.set(fn.qid, result);
186
+ // Seed the cache with this function's OWN summary, keyed by the state
187
+ // actually analyzed. This can overwrite an entry a callee-triggered
188
+ // lazy compute() wrote for the same key; the overwrite is the more
189
+ // precise answer (it resolves bare-identifier calls against fn.file).
190
+ // With the E1 key fix this is a no-op difference when no seed hook is
191
+ // supplied. See DESIGN_GRAPH_BUILDER.md section 3.6.
192
+ cache.set(fn.qid, entryState, summaryFromAnalysisResult(result));
193
+ continue;
194
+ }
195
+
196
+ // Recursive component: solve to a fixed point or report partial.
197
+ const entryStates = new Map(comp.members.map(q => [q, entryStateOf(fnByQid.get(q))]));
198
+ const keepHashes = new Map(comp.members.map(q => [q, new Set([hashState(entryStates.get(q))])]));
199
+ const solved = solveRecursiveComponent({
200
+ members: comp.members,
201
+ maxRounds: opts.maxRounds,
202
+ maxPathDepth: opts.maxPathDepth,
203
+ maxChangedFacts: opts.maxChangedFacts,
204
+ shouldStop,
205
+ beforeRound() {
206
+ // Contexts reached through call-site argument states were computed
207
+ // against an earlier round's summaries. Forget them; the seeded
208
+ // entry context stays and is overwritten by `publish`.
209
+ for (const q of comp.members) cache.dropContexts(q, keepHashes.get(q));
210
+ },
211
+ publish(q, summary) {
212
+ cache.set(q, entryStates.get(q), summary);
213
+ },
214
+ analyze(q) {
215
+ const fn = fnByQid.get(q);
216
+ work++;
217
+ progress(fn);
218
+ let result;
219
+ const raw = cache.refreshing(q, entryStates.get(q), () => cache.compute(q, entryStates.get(q), (es) => {
220
+ result = analyzeFunctionFieldIdentity(fn, es, ctxFor(fn));
221
+ return summaryFromAnalysisResult(result);
222
+ }));
223
+ results.set(q, result);
224
+ return raw;
225
+ },
226
+ });
227
+ for (const r of solved.wideningReasons) wideningReasons.add(r);
228
+ convergence.rounds = Math.max(convergence.rounds, solved.rounds);
229
+ convergence.changedFactsTotal += solved.changedFactsTotal;
230
+ for (const f of solved.changedFacts) convergence.changedFacts.push({ component: comp.id, ...f });
231
+ convergence.components.push({
232
+ id: comp.id,
233
+ members: comp.members,
234
+ rounds: solved.rounds,
235
+ converged: solved.converged,
236
+ stoppedBy: solved.stoppedBy,
237
+ changedFacts: solved.changedFactsTotal,
238
+ });
239
+ if (!solved.converged) {
240
+ for (const q of solved.remainingFrontier) frontier.add(q);
241
+ if (solved.stoppedBy && !convergence.stoppedBy) convergence.stoppedBy = solved.stoppedBy;
242
+ }
243
+ }
244
+ if (stopReason) convergence.stoppedBy = stopReason;
245
+
246
+ // Anything never reached because a budget stopped the run is frontier too.
247
+ if (stopReason) for (const fn of fnList) if (!results.has(fn.qid)) frontier.add(fn.qid);
248
+ convergence.remainingFrontier = [...frontier].sort();
249
+ convergence.wideningReasons = [...wideningReasons].sort();
250
+ if (convergence.stoppedBy || convergence.remainingFrontier.length > 0) {
251
+ convergence.converged = false;
252
+ convergence.status = 'partial';
253
+ }
254
+
255
+ // Keep `results` in ascending-qid order regardless of schedule order.
256
+ const ordered = new Map();
257
+ for (const fn of fnList) if (results.has(fn.qid)) ordered.set(fn.qid, results.get(fn.qid));
258
+
259
+ // DAG-07 completion roll-up: per-function budget stops, degraded
260
+ // contexts and cancellation, so a cut-short unit is visible even when it
261
+ // emitted nothing.
262
+ const incomplete = [];
263
+ let cancelled = false;
264
+ for (const fn of fnList) {
265
+ const r = ordered.get(fn.qid);
266
+ if (!r || !r.completion || r.completion.status === 'complete') continue;
267
+ if (r.completion.status === 'cancelled') cancelled = true;
268
+ incomplete.push({
269
+ qid: fn.qid,
270
+ context: hashState(entryStateOf(fn)),
271
+ reasons: r.completion.reasons,
272
+ iterations: r.completion.iterations,
273
+ maxIterations: r.completion.maxIterations,
274
+ unfinishedWork: r.completion.unfinishedWork,
275
+ });
140
276
  }
277
+ if (stopReason === 'aborted' || stopReason === 'deadline') cancelled = true;
278
+ const notAnalyzed = fnList.length - ordered.size;
279
+ const seen = new Set();
280
+ const degraded = cache.completionNotes().slice(notesFrom).filter((n) => {
281
+ const k = `${n.qid}:${n.reason}`;
282
+ if (seen.has(k)) return false;
283
+ seen.add(k);
284
+ return true;
285
+ });
286
+ const completion = {
287
+ status: cancelled ? 'cancelled'
288
+ : (incomplete.length === 0 && degraded.length === 0 && convergence.status === 'complete') ? 'complete' : 'partial',
289
+ functionsAnalyzed: ordered.size,
290
+ functionsNotAnalyzed: notAnalyzed,
291
+ incomplete,
292
+ degraded,
293
+ };
141
294
 
142
- return { results, cache };
295
+ return { results: ordered, cache, convergence, completion };
143
296
  }