@clear-capabilities/agentic-security-scanner 0.183.1 → 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.
- package/CHANGELOG.md +29 -0
- package/bin/agentic-security.js +88 -0
- package/dist/{2a8456e0800aeee871b6.json → 0b301d45b231f144a1b4.json} +27 -9
- package/dist/1128.index.js +908 -0
- package/dist/1336.index.js +261 -112
- package/dist/1665.index.js +828 -0
- package/dist/2923.index.js +17 -9
- package/dist/9382.index.js +23 -2
- package/dist/9579.index.js +11 -4
- package/dist/agentic-security.mjs +15 -15
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/frontend/src/shell.js +12 -2
- package/package.json +27 -9
- package/src/context-intelligence/controls.js +15 -0
- package/src/context-intelligence/reachability.js +8 -2
- package/src/dataflow/CLAUDE.md +43 -1
- package/src/dataflow/backward.js +42 -7
- package/src/dataflow/cross-service-taint.js +83 -1
- package/src/dataflow/demand-query.js +929 -0
- package/src/dataflow/dep-fingerprint.js +718 -0
- package/src/dataflow/engine.js +48 -6
- package/src/dataflow/ifds-precise.js +22 -0
- package/src/dataflow/incremental-replay.js +118 -0
- package/src/dataflow/incremental-session.js +257 -0
- package/src/dataflow/incremental.js +237 -35
- package/src/dataflow/index.js +25 -94
- package/src/dataflow/summaries.js +72 -6
- package/src/dataflow/tabulation.js +80 -24
- package/src/discovery/hunter.js +6 -2
- package/src/discovery/index.js +5 -2
- package/src/engine.js +63 -3
- package/src/graph/CLAUDE.md +31 -0
- package/src/graph/adapters/artifact.js +81 -0
- package/src/graph/adapters/crossrepo.js +75 -0
- package/src/graph/adapters/dataflow.js +76 -0
- package/src/graph/adapters/provenance.js +92 -0
- package/src/graph/adapters/semantic.js +89 -0
- package/src/graph/crosswalk-builders.js +152 -0
- package/src/graph/crosswalk.js +146 -0
- package/src/graph/index.js +15 -0
- package/src/graph/observation.js +57 -0
- package/src/graph/refs.js +105 -0
- package/src/graph/registry.js +129 -0
- package/src/graph/relations.js +81 -0
- package/src/graph/scc.js +241 -0
- package/src/graph/view.js +254 -0
- package/src/language/haskell-disclosure.js +13 -1
- package/src/lineage/CLAUDE.md +71 -8
- package/src/lineage/DESIGN_GRAPH_BUILDER.md +1 -1
- package/src/lineage/DESIGN_PATH_PROVENANCE.md +6 -0
- package/src/lineage/boundary-contract.js +273 -0
- package/src/lineage/completion.js +205 -0
- package/src/lineage/continuation.js +140 -0
- package/src/lineage/control-credit.js +313 -0
- package/src/lineage/correlation.js +199 -0
- package/src/lineage/coverage.js +8 -1
- package/src/lineage/cross-repo-link.js +22 -0
- package/src/lineage/cut-planner.js +548 -0
- package/src/lineage/driver.js +219 -66
- package/src/lineage/engine.js +166 -21
- package/src/lineage/export-json.js +5 -0
- package/src/lineage/graph-builder.js +84 -7
- package/src/lineage/haskell-view.js +59 -0
- package/src/lineage/index.js +3 -0
- package/src/lineage/nix-view.js +35 -0
- package/src/lineage/observation-adapters.js +17 -9
- package/src/lineage/owned-build.js +77 -0
- package/src/lineage/owned-worker-entry.js +22 -0
- package/src/lineage/owned-worker.js +95 -0
- package/src/lineage/path-evidence.js +123 -0
- package/src/lineage/path-predicates.js +529 -0
- package/src/lineage/path-query.js +189 -20
- package/src/lineage/path-store.js +191 -17
- package/src/lineage/scc-solver.js +186 -0
- package/src/lineage/smt-orchestrator.js +138 -0
- package/src/lineage/source-seeding.js +36 -2
- package/src/lineage/summaries.js +66 -4
- package/src/lineage/value-versions.js +130 -0
- package/src/lineage/witness-selection.js +95 -0
- package/src/pipeline/scan-health.js +19 -0
- package/src/posture/CLAUDE.md +13 -0
- package/src/posture/evidence-bundle.js +1 -1
- package/src/posture/witness-bundle.js +573 -0
- package/src/posture/witness-runtime.js +276 -0
- package/src/report/index.js +2 -0
package/src/lineage/driver.js
CHANGED
|
@@ -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
|
|
70
|
-
//
|
|
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
|
-
|
|
79
|
-
|
|
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,
|
|
83
|
-
// opts.recordHop
|
|
84
|
-
//
|
|
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
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
}
|