@clear-capabilities/agentic-security-scanner 0.184.0 → 0.186.1
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 +37 -0
- package/bin/agentic-security.js +111 -1
- package/dist/1128.index.js +912 -0
- package/dist/1665.index.js +828 -0
- package/dist/2584.index.js +1 -1
- package/dist/4171.index.js +2 -2
- package/dist/5752.index.js +466 -0
- package/dist/7405.index.js +1 -1
- package/dist/8158.index.js +1713 -0
- package/dist/8798.index.js +1 -1
- package/dist/9382.index.js +2 -2
- package/dist/9579.index.js +1 -1
- package/dist/agentic-security.mjs +29 -29
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/{dad58d1113a12c23a142.json → ca62ee93079be4c24eee.json} +29 -12
- package/dist/frontend/src/shell.js +12 -2
- package/package.json +29 -12
- package/src/dataflow/CLAUDE.md +46 -1
- package/src/dataflow/access-paths.js +12 -2
- package/src/dataflow/backward.js +42 -7
- package/src/dataflow/cross-service-taint.js +83 -1
- package/src/dataflow/demand-query.js +1168 -0
- package/src/dataflow/dep-fingerprint.js +718 -0
- package/src/dataflow/engine.js +48 -6
- package/src/dataflow/exploit-prover.js +6 -2
- 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/ir-cache.js +268 -0
- package/src/dataflow/privacy-taint.js +7 -0
- package/src/dataflow/summaries.js +72 -6
- package/src/dataflow/tabulation.js +80 -24
- package/src/engine.js +113 -8
- 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/ir/CLAUDE.md +4 -0
- package/src/ir/callgraph.js +36 -8
- package/src/ir/index.js +18 -4
- package/src/ir/parser-js.js +10 -0
- package/src/language/CLAUDE.md +13 -0
- package/src/language/nix-option-types.js +170 -0
- package/src/language/nix-script-taint.js +46 -3
- package/src/language/nix-secrets.js +20 -2
- package/src/lineage/CLAUDE.md +72 -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/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 +174 -0
- package/src/lineage/smt-z3-backend.js +294 -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 +18 -0
- package/src/posture/CLAUDE.md +17 -0
- package/src/posture/business-logic.js +16 -7
- package/src/posture/dag-commands.js +442 -0
- package/src/posture/evidence-bundle.js +1 -1
- package/src/posture/finding-defaults.js +19 -0
- package/src/posture/git-history.js +91 -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/sast/CLAUDE.md +2 -0
- package/src/sast/_call-args.js +70 -0
- package/src/sast/init-scope.js +87 -0
- package/src/sast/listen-bind-address.js +49 -0
- package/src/sast/redirect-target.js +106 -0
- package/src/sast/reflected-input.js +76 -0
- package/src/sast/send-file-root.js +27 -0
- package/src/sast/sensitive-data-logging.js +38 -1
- package/dist/119.index.js +0 -197
- package/dist/128.index.js +0 -129
- package/dist/1293.index.js +0 -5689
- package/dist/1336.index.js +0 -16182
- package/dist/1351.index.js +0 -133
- package/dist/1387.index.js +0 -271
- package/dist/1437.index.js +0 -23
- package/dist/1445.index.js +0 -110
- package/dist/1505.index.js +0 -151
- package/dist/1557.index.js +0 -293
- package/dist/1621.index.js +0 -311
- package/dist/1751.index.js +0 -160
- package/dist/1765.index.js +0 -727
- package/dist/1861.index.js +0 -690
- package/dist/1906.index.js +0 -433
- package/dist/208.index.js +0 -1631
- package/dist/2223.index.js +0 -159
- package/dist/2238.index.js +0 -295
- package/dist/2530.index.js +0 -152
- package/dist/2565.index.js +0 -116
- package/dist/2574.index.js +0 -127
- package/dist/2620.index.js +0 -345
- package/dist/2773.index.js +0 -98
- package/dist/2775.index.js +0 -32
- package/dist/2966.index.js +0 -171
- package/dist/2972.index.js +0 -87
- package/dist/2991.index.js +0 -131
- package/dist/3063.index.js +0 -833
- package/dist/3075.index.js +0 -227
- package/dist/3121.index.js +0 -297
- package/dist/3314.index.js +0 -263
- package/dist/3480.index.js +0 -1822
- package/dist/3481.index.js +0 -24
- package/dist/3506.index.js +0 -268
- package/dist/3533.index.js +0 -591
- package/dist/3653.index.js +0 -450
- package/dist/3740.index.js +0 -722
- package/dist/3822.index.js +0 -131
- package/dist/3868.index.js +0 -292
- package/dist/3918.index.js +0 -192
- package/dist/4090.index.js +0 -288
- package/dist/4113.index.js +0 -1286
- package/dist/4121.index.js +0 -1190
- package/dist/4142.index.js +0 -690
- package/dist/4168.index.js +0 -509
- package/dist/4281.index.js +0 -993
- package/dist/4336.index.js +0 -436
- package/dist/4387.index.js +0 -532
- package/dist/4413.index.js +0 -302
- package/dist/4476.index.js +0 -4510
- package/dist/4491.index.js +0 -133
- package/dist/4538.index.js +0 -185
- package/dist/4684.index.js +0 -319
- package/dist/4740.index.js +0 -165
- package/dist/4780.index.js +0 -209
- package/dist/4802.index.js +0 -225
- package/dist/4812.index.js +0 -379
- package/dist/4835.index.js +0 -126
- package/dist/494.index.js +0 -226
- package/dist/4991.index.js +0 -549
- package/dist/5199.index.js +0 -305
- package/dist/5216.index.js +0 -109
- package/dist/5439.index.js +0 -301
- package/dist/5443.index.js +0 -97
- package/dist/5496.index.js +0 -377
- package/dist/5738.index.js +0 -240
- package/dist/5801.index.js +0 -588
- package/dist/5817.index.js +0 -714
- package/dist/5858.index.js +0 -223
- package/dist/5933.index.js +0 -143
- package/dist/601.index.js +0 -23
- package/dist/6173.index.js +0 -273
- package/dist/6291.index.js +0 -1871
- package/dist/6312.index.js +0 -666
- package/dist/634.index.js +0 -16182
- package/dist/644.index.js +0 -87
- package/dist/6508.index.js +0 -283
- package/dist/6560.index.js +0 -645
- package/dist/6576.index.js +0 -422
- package/dist/6579.index.js +0 -271
- package/dist/6655.index.js +0 -3221
- package/dist/6745.index.js +0 -305
- package/dist/6818.index.js +0 -21642
- package/dist/6932.index.js +0 -1190
- package/dist/6972.index.js +0 -167
- package/dist/6995.index.js +0 -163
- package/dist/7249.index.js +0 -191
- package/dist/7372.index.js +0 -1197
- package/dist/7546.index.js +0 -541
- package/dist/76.index.js +0 -448
- package/dist/7879.index.js +0 -163
- package/dist/8025.index.js +0 -130
- package/dist/8054.index.js +0 -248
- package/dist/808.index.js +0 -1330
- package/dist/8097.index.js +0 -201
- package/dist/8126.index.js +0 -191
- package/dist/8178.index.js +0 -78
- package/dist/8382.index.js +0 -93
- package/dist/8408.index.js +0 -261
- package/dist/8540.index.js +0 -174
- package/dist/8635.index.js +0 -802
- package/dist/8688.index.js +0 -410
- package/dist/8699.index.js +0 -779
- package/dist/8891.index.js +0 -457
- package/dist/8916.index.js +0 -885
- package/dist/9146.index.js +0 -170
- package/dist/9168.index.js +0 -841
- package/dist/9292.index.js +0 -369
- package/dist/9422.index.js +0 -866
- package/dist/9440.index.js +0 -453
- package/dist/9479.index.js +0 -152
- package/dist/9481.index.js +0 -220
- package/dist/9539.index.js +0 -100
- package/dist/9547.index.js +0 -103
- package/dist/9692.index.js +0 -90
- package/dist/9800.index.js +0 -462
- package/dist/9899.index.js +0 -138
- package/dist/9981.index.js +0 -206
- package/dist/999.index.js +0 -117
- package/dist/9999.index.js +0 -1698
|
@@ -153,9 +153,13 @@ export function canonicalize(set) {
|
|
|
153
153
|
const sorted = [...set].sort((a, b) => a.length - b.length || (a < b ? -1 : 1));
|
|
154
154
|
const out = new Set();
|
|
155
155
|
for (const path of sorted) {
|
|
156
|
+
// A kept path covers `path` exactly when it is one of the dot-delimited prefixes of it, so look those up
|
|
157
|
+
// (a handful per path) instead of testing every kept path (the set can hold hundreds in a large function).
|
|
156
158
|
let covered = false;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
+
if (typeof path === 'string') {
|
|
160
|
+
for (let i = path.indexOf('.'); i >= 0; i = path.indexOf('.', i + 1)) {
|
|
161
|
+
if (out.has(path.slice(0, i))) { covered = true; break; }
|
|
162
|
+
}
|
|
159
163
|
}
|
|
160
164
|
if (!covered) out.add(path);
|
|
161
165
|
}
|
|
@@ -176,6 +180,12 @@ export function hashSet(set) {
|
|
|
176
180
|
export function setsEqual(a, b) {
|
|
177
181
|
if (a === b) return true;
|
|
178
182
|
if (!a || !b) return false;
|
|
183
|
+
// Identical members are equal under any canonicalization; only a difference needs the canonical forms.
|
|
184
|
+
if (a.size === b.size) {
|
|
185
|
+
let same = true;
|
|
186
|
+
for (const x of a) if (!b.has(x)) { same = false; break; }
|
|
187
|
+
if (same) return true;
|
|
188
|
+
}
|
|
179
189
|
const ca = canonicalize(a), cb = canonicalize(b);
|
|
180
190
|
if (ca.size !== cb.size) return false;
|
|
181
191
|
for (const x of ca) if (!cb.has(x)) return false;
|
package/src/dataflow/backward.js
CHANGED
|
@@ -49,6 +49,15 @@ function _reverseEdges(cfg) {
|
|
|
49
49
|
return rev;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* CFG reverse-edge index (node id -> predecessor ids), exported so the demand
|
|
54
|
+
* selector (demand-query.js) and this slicer share one definition of the
|
|
55
|
+
* incoming-edge relation instead of each deriving its own.
|
|
56
|
+
*/
|
|
57
|
+
export function reverseEdgesOf(cfg) {
|
|
58
|
+
return _reverseEdges(cfg);
|
|
59
|
+
}
|
|
60
|
+
|
|
52
61
|
/**
|
|
53
62
|
* Build a backward slice from a finding's sink site to its source(s).
|
|
54
63
|
*
|
|
@@ -75,10 +84,28 @@ function _reverseEdges(cfg) {
|
|
|
75
84
|
* ]
|
|
76
85
|
*/
|
|
77
86
|
export function sliceBackward(fn, sinkNode, sinkArgPath) {
|
|
87
|
+
return sliceBackwardWithCompletion(fn, sinkNode, sinkArgPath).slice;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Same slice, plus a completion record (DAG-07 R07.1): { slice, completion }.
|
|
92
|
+
* A slice that stopped at SLICE_BUDGET_NODES is a prefix of the real
|
|
93
|
+
* backward closure; the record carries how many queued steps were left, so a
|
|
94
|
+
* consumer (the SMT infeasibility check among them) can tell "no source
|
|
95
|
+
* found" from "stopped looking". `sliceBackward` itself still returns the
|
|
96
|
+
* bare array for existing callers, with the same record attached as a
|
|
97
|
+
* non-enumerable `completion` property.
|
|
98
|
+
*/
|
|
99
|
+
export function sliceBackwardWithCompletion(fn, sinkNode, sinkArgPath) {
|
|
78
100
|
const out = [];
|
|
79
|
-
|
|
101
|
+
const completion = { status: 'complete', reasons: [], unexploredFrontierCount: 0, visited: 0, limits: { maxNodes: SLICE_BUDGET_NODES } };
|
|
102
|
+
const finish = () => {
|
|
103
|
+
Object.defineProperty(out, 'completion', { value: completion, enumerable: false, configurable: true });
|
|
104
|
+
return { slice: out, completion };
|
|
105
|
+
};
|
|
106
|
+
if (!fn || !sinkNode) { completion.status = 'unavailable'; completion.reasons = ['no-function-or-sink']; return finish(); }
|
|
80
107
|
const cfg = fn.cfg;
|
|
81
|
-
if (!cfg || !cfg.nodes) return
|
|
108
|
+
if (!cfg || !cfg.nodes) { completion.status = 'unavailable'; completion.reasons = ['no-cfg']; return finish(); }
|
|
82
109
|
const rev = _reverseEdges(cfg);
|
|
83
110
|
|
|
84
111
|
// Map node-id to itself lookup for nodes in this CFG.
|
|
@@ -90,7 +117,7 @@ export function sliceBackward(fn, sinkNode, sinkArgPath) {
|
|
|
90
117
|
for (const id of Object.keys(nodes)) {
|
|
91
118
|
if (nodes[id] === sinkNode) { sinkNid = id; break; }
|
|
92
119
|
}
|
|
93
|
-
if (!sinkNid) return
|
|
120
|
+
if (!sinkNid) { completion.status = 'unavailable'; completion.reasons = ['sink-not-in-cfg']; return finish(); }
|
|
94
121
|
|
|
95
122
|
out.push({
|
|
96
123
|
line: sinkNode.line || 0,
|
|
@@ -104,7 +131,12 @@ export function sliceBackward(fn, sinkNode, sinkArgPath) {
|
|
|
104
131
|
let visitedCount = 0;
|
|
105
132
|
|
|
106
133
|
while (work.length) {
|
|
107
|
-
if (++visitedCount > SLICE_BUDGET_NODES)
|
|
134
|
+
if (++visitedCount > SLICE_BUDGET_NODES) {
|
|
135
|
+
completion.status = 'partial';
|
|
136
|
+
completion.reasons = ['slice-node-budget'];
|
|
137
|
+
completion.unexploredFrontierCount = work.length;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
108
140
|
const { nid, queryPath } = work.shift();
|
|
109
141
|
const key = `${nid}::${queryPath}`;
|
|
110
142
|
if (visited.has(key)) continue;
|
|
@@ -206,8 +238,10 @@ export function sliceBackward(fn, sinkNode, sinkArgPath) {
|
|
|
206
238
|
}
|
|
207
239
|
}
|
|
208
240
|
|
|
241
|
+
completion.visited = Math.min(visitedCount, SLICE_BUDGET_NODES);
|
|
209
242
|
// Reverse so the trace reads source-first.
|
|
210
|
-
|
|
243
|
+
out.reverse();
|
|
244
|
+
return finish();
|
|
211
245
|
}
|
|
212
246
|
|
|
213
247
|
/**
|
|
@@ -233,7 +267,7 @@ export function annotateBackwardSlices(findings, perFileIR, callGraph) {
|
|
|
233
267
|
if (callGraph && callGraph.functions) {
|
|
234
268
|
for (const fn of callGraph.functions.values()) fnByQid.set(fn.qid, fn);
|
|
235
269
|
}
|
|
236
|
-
let annotated = 0, skipped = 0, exhausted = false;
|
|
270
|
+
let annotated = 0, skipped = 0, exhausted = false, partialSlices = 0;
|
|
237
271
|
for (const f of findings) {
|
|
238
272
|
if (Date.now() > deadline) { exhausted = true; skipped++; continue; }
|
|
239
273
|
if (!f || !f._funcQid) { skipped++; continue; }
|
|
@@ -278,6 +312,7 @@ export function annotateBackwardSlices(findings, perFileIR, callGraph) {
|
|
|
278
312
|
if (!slice) slice = candidateSlice;
|
|
279
313
|
}
|
|
280
314
|
}
|
|
315
|
+
if (slice && slice.completion && slice.completion.status !== 'complete') partialSlices++;
|
|
281
316
|
if (slice && slice.length) {
|
|
282
317
|
f.backwardSlice = slice;
|
|
283
318
|
f.pathSteps = (f.pathSteps || []).concat(slice.map(s => ({
|
|
@@ -291,7 +326,7 @@ export function annotateBackwardSlices(findings, perFileIR, callGraph) {
|
|
|
291
326
|
}
|
|
292
327
|
}
|
|
293
328
|
Object.defineProperty(findings, '_annotateBackwardSlicesStats', {
|
|
294
|
-
value: { annotated, skipped, exhausted, budgetMs },
|
|
329
|
+
value: { annotated, skipped, exhausted, budgetMs, partialSlices },
|
|
295
330
|
enumerable: false,
|
|
296
331
|
});
|
|
297
332
|
return findings;
|
|
@@ -63,6 +63,8 @@ import * as path from 'node:path';
|
|
|
63
63
|
import * as yaml from '../util/yaml.js';
|
|
64
64
|
|
|
65
65
|
import { statePath } from '../posture/state-dir.js';
|
|
66
|
+
import { runTaintEngine } from './engine.js';
|
|
67
|
+
import { bindBoundaryContracts, boundaryAttribution, BOUNDARY_CONTRACTS_FILENAME, validateBoundaryContract } from '../lineage/boundary-contract.js';
|
|
66
68
|
const SERVICES_FILE_NAMES = ['services.yml', 'services.yaml'];
|
|
67
69
|
|
|
68
70
|
export function loadServiceGraph(scanRoot) {
|
|
@@ -342,4 +344,84 @@ export function runCrossServiceTaint(scanRoot, findings) {
|
|
|
342
344
|
return annotateCrossServiceFindings(findings, graph, current);
|
|
343
345
|
}
|
|
344
346
|
|
|
345
|
-
|
|
347
|
+
// ─── DAG-08: boundary-contract consumer seeding ──────────────────────────────
|
|
348
|
+
//
|
|
349
|
+
// The SCOPE NOTE above stays true of the `consumes` annotation pass: it never invents a source. This section is the separate,
|
|
350
|
+
// explicit mechanism that does. A versioned boundary contract (lineage/boundary-contract.js) that BINDS to a parser-resolved
|
|
351
|
+
// consumer function and parameter seeds the declared, mapped fields of that parameter as a MODELED untrusted input. The seed is
|
|
352
|
+
// handed to the engine through `fn.boundarySeeds` (read by engine.js's `_unionAnnotationTaint`) for the duration of one run and
|
|
353
|
+
// always removed afterwards, so the shared call graph is never left seeded.
|
|
354
|
+
//
|
|
355
|
+
// A finding produced this way carries `boundary: {..., modeledInput: true, observed: false, exploited: false}`. A declaration
|
|
356
|
+
// establishes where taint is assumed to enter; it never establishes that a value was seen or that the sink is exploitable. A
|
|
357
|
+
// contract that does not bind produces a coverage gap and NO finding.
|
|
358
|
+
|
|
359
|
+
const DEFAULT_MAX_BOUNDARY_RUNS = 64;
|
|
360
|
+
|
|
361
|
+
function loadBoundaryContracts(scanRoot) {
|
|
362
|
+
if (!scanRoot) return { contracts: [], errors: [] };
|
|
363
|
+
const fp = statePath(scanRoot, BOUNDARY_CONTRACTS_FILENAME);
|
|
364
|
+
if (!fs.existsSync(fp)) return { contracts: [], errors: [] };
|
|
365
|
+
try {
|
|
366
|
+
const doc = JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
367
|
+
const list = Array.isArray(doc) ? doc : Array.isArray(doc && doc.contracts) ? doc.contracts : [];
|
|
368
|
+
return { contracts: list, errors: [] };
|
|
369
|
+
} catch (e) {
|
|
370
|
+
return { contracts: [], errors: [`Failed to parse ${fp}: ${e.message}`] };
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Run the taint engine with each bound contract's consumer fields seeded, one seed at a time so a finding is attributed to the
|
|
376
|
+
* exact consumer field that reached it (a sibling field the contract does not map is never seeded and so never attributed).
|
|
377
|
+
* When `contracts` is null/undefined they are read from `<scanRoot>/.agentic-security/boundary-contracts.json` (opts.scanRoot).
|
|
378
|
+
* @returns {{findings: object[], bindings: object[], gaps: object[], rejected: object[], coverage: object, loadErrors: string[]}}
|
|
379
|
+
*/
|
|
380
|
+
export function runBoundaryTaint(perFileIR, callGraph, contracts, opts = {}) {
|
|
381
|
+
let loadErrors = [];
|
|
382
|
+
if (contracts === null || contracts === undefined) { const l = loadBoundaryContracts(opts.scanRoot); contracts = l.contracts; loadErrors = l.errors; }
|
|
383
|
+
const maxRuns = Number.isFinite(opts.maxRuns) ? opts.maxRuns : DEFAULT_MAX_BOUNDARY_RUNS;
|
|
384
|
+
const { bindings, gaps, rejected } = bindBoundaryContracts(contracts, callGraph);
|
|
385
|
+
const fnByQid = new Map();
|
|
386
|
+
for (const f of callGraph.functions.values()) fnByQid.set(f.qid, f);
|
|
387
|
+
const baseline = new Set(runTaintEngine(perFileIR, callGraph, opts.engine || {}).map((f) => f.id));
|
|
388
|
+
const found = new Map();
|
|
389
|
+
let runs = 0; let truncated = false;
|
|
390
|
+
for (const b of bindings) {
|
|
391
|
+
const fn = fnByQid.get(b.qid);
|
|
392
|
+
for (const seed of b.seeds) {
|
|
393
|
+
if (runs >= maxRuns) { truncated = true; break; }
|
|
394
|
+
runs++;
|
|
395
|
+
const prior = fn.boundarySeeds;
|
|
396
|
+
fn.boundarySeeds = [seed];
|
|
397
|
+
let out = [];
|
|
398
|
+
try { out = runTaintEngine(perFileIR, callGraph, opts.engine || {}); } finally { if (prior === undefined) delete fn.boundarySeeds; else fn.boundarySeeds = prior; }
|
|
399
|
+
const mapping = b.mappedFields.find((m) => seed.endsWith(`.${m.to}`) || seed === m.to) || null;
|
|
400
|
+
for (const f of out) {
|
|
401
|
+
if (baseline.has(f.id)) continue;
|
|
402
|
+
let rec = found.get(f.id);
|
|
403
|
+
if (!rec) { rec = { finding: { ...f }, attributions: [] }; found.set(f.id, rec); }
|
|
404
|
+
rec.attributions.push({ binding: b, seed, mapping });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (truncated) break;
|
|
408
|
+
}
|
|
409
|
+
const findings = [...found.values()].map(({ finding, attributions }) => {
|
|
410
|
+
const first = attributions[0];
|
|
411
|
+
finding.boundary = {
|
|
412
|
+
...boundaryAttribution(first.binding),
|
|
413
|
+
fields: attributions.map((a) => ({ seed: a.seed, producerField: a.mapping && a.mapping.from, consumerField: a.mapping && a.mapping.to })),
|
|
414
|
+
discoveredBy: 'boundary-contract-seed',
|
|
415
|
+
};
|
|
416
|
+
finding.boundarySeeded = true;
|
|
417
|
+
return finding;
|
|
418
|
+
}).sort((a, b) => (a.id < b.id ? -1 : 1));
|
|
419
|
+
const allGaps = [...gaps];
|
|
420
|
+
if (truncated) allGaps.push({ kind: 'budget', reason: 'engine-run-budget', detail: `stopped after ${maxRuns} seeded runs; remaining seeds were not analyzed` });
|
|
421
|
+
return {
|
|
422
|
+
findings, bindings, gaps: allGaps, rejected, loadErrors,
|
|
423
|
+
coverage: { contracts: (contracts || []).length, bound: bindings.length, unbound: gaps.filter((g) => g.side === 'consumer').length, rejected: rejected.length, engineRuns: runs, truncated },
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export const _internals = { _normalizeGraph, SERVICES_FILE_NAMES, _pathTemplateMatches, _escapeRegex, validateBoundaryContract };
|