gitnexus 1.6.8-rc.21 → 1.6.8-rc.23
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/dist/core/incremental/subgraph-extract.js +12 -1
- package/dist/core/ingestion/pipeline-phases/index.d.ts +1 -0
- package/dist/core/ingestion/pipeline-phases/index.js +1 -0
- package/dist/core/ingestion/pipeline-phases/taint-summaries.d.ts +30 -0
- package/dist/core/ingestion/pipeline-phases/taint-summaries.js +81 -0
- package/dist/core/ingestion/pipeline.d.ts +13 -0
- package/dist/core/ingestion/pipeline.js +5 -1
- package/dist/core/ingestion/scope-resolution/pipeline/phase.d.ts +7 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.js +15 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +17 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +37 -1
- package/dist/core/ingestion/taint/interproc-emit.d.ts +50 -0
- package/dist/core/ingestion/taint/interproc-emit.js +88 -0
- package/dist/core/ingestion/taint/interproc-solver.d.ts +110 -0
- package/dist/core/ingestion/taint/interproc-solver.js +312 -0
- package/dist/core/ingestion/taint/propagate.d.ts +1 -1
- package/dist/core/ingestion/taint/propagate.js +4 -9
- package/dist/core/ingestion/taint/source-sink-config.d.ts +13 -0
- package/dist/core/ingestion/taint/source-sink-config.js +24 -1
- package/dist/core/ingestion/taint/summary-harvest-driver.d.ts +56 -0
- package/dist/core/ingestion/taint/summary-harvest-driver.js +118 -0
- package/dist/core/ingestion/taint/summary-harvest.d.ts +89 -0
- package/dist/core/ingestion/taint/summary-harvest.js +537 -0
- package/dist/core/ingestion/taint/summary-model.d.ts +200 -0
- package/dist/core/ingestion/taint/summary-model.js +86 -0
- package/dist/core/lbug/lbug-adapter.d.ts +18 -0
- package/dist/core/lbug/lbug-adapter.js +55 -0
- package/dist/core/run-analyze.d.ts +7 -1
- package/dist/core/run-analyze.js +21 -1
- package/dist/mcp/local/local-backend.js +93 -10
- package/dist/mcp/tools.js +7 -6
- package/dist/storage/repo-manager.d.ts +10 -0
- package/hooks/antigravity/gitnexus-antigravity-hook.cjs +89 -3
- package/hooks/claude/gitnexus-hook.cjs +88 -4
- package/hooks/claude/hook-db-lock-probe.cjs +53 -21
- package/package.json +1 -1
- package/skills/gitnexus-taint-analysis.md +178 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-function taint SUMMARY model (#2084 M4 U2).
|
|
3
|
+
*
|
|
4
|
+
* A {@link FunctionSummary} is the compact, context-insensitive abstraction of
|
|
5
|
+
* one function's taint behaviour — the input to the interprocedural fixpoint
|
|
6
|
+
* (`interproc-solver.ts`). It is the GitNexus analogue of Pysa's `.pysa`
|
|
7
|
+
* models, Mariana Trench's "propagations", and CodeQL Models-as-Data summary
|
|
8
|
+
* rows: a function is reduced to how taint enters (params / generated sources),
|
|
9
|
+
* how it moves through (param→return, param→callee-arg), and where it lands
|
|
10
|
+
* (param→sink). The fixpoint composes these across resolved `CALLS` edges so a
|
|
11
|
+
* source in one function reaches a sink in another.
|
|
12
|
+
*
|
|
13
|
+
* ## Why summaries (not whole-program IFDS)
|
|
14
|
+
*
|
|
15
|
+
* The functional/summary method (Sharir-Pnueli 1981) analyses each function
|
|
16
|
+
* ONCE and propagates the result over the call graph — the same shape Pysa,
|
|
17
|
+
* Mariana Trench, and Infer use in production. GitNexus already resolves the
|
|
18
|
+
* call graph (`CALLS` edges carry final node ids), so the summary IS the only
|
|
19
|
+
* new artifact; propagation is graph reachability over a finite lattice.
|
|
20
|
+
*
|
|
21
|
+
* ## Granularity (first cut)
|
|
22
|
+
*
|
|
23
|
+
* WHOLE-PARAMETER. Ports are `param i`, `return`, and `receiver` — no field
|
|
24
|
+
* access paths (`arg0.field.sub`). Field sensitivity, callback-parameter ports
|
|
25
|
+
* (`Argument[0].Parameter[0]`), and context sensitivity are deferred (plan
|
|
26
|
+
* KTD; the largest JS/TS FN class — closures — stays a documented gap).
|
|
27
|
+
*
|
|
28
|
+
* ## Plain-data discipline
|
|
29
|
+
*
|
|
30
|
+
* A summary is a JSON-plain value type (no functions, class instances, Maps, or
|
|
31
|
+
* Symbols) so it survives `RunScopeResolutionStats` → `ScopeResolutionOutput`
|
|
32
|
+
* threading and any future worker/cache boundary unchanged — the same
|
|
33
|
+
* `Cloneable` constraint the CFG side channel obeys.
|
|
34
|
+
*/
|
|
35
|
+
import type { SinkKind, SourceKind } from './source-sink-config.js';
|
|
36
|
+
/**
|
|
37
|
+
* Source-relative parameter index (0-based, in declaration order). A
|
|
38
|
+
* function's first parameter is `0`. Destructured / rest params map each bound
|
|
39
|
+
* name to the index of the formal parameter that introduced it (so
|
|
40
|
+
* `function f([a, b]) {}` binds both `a` and `b` to param `0`).
|
|
41
|
+
*/
|
|
42
|
+
export type ParamIndex = number;
|
|
43
|
+
/**
|
|
44
|
+
* `param i` flows into argument `argIndex` of a call at source line `callLine`.
|
|
45
|
+
* The interprocedural solver joins this to the caller's outgoing `CALLS` edges
|
|
46
|
+
* by CALLEE NAME (`calleeName`) — NOT by `callLine` — because line-base parity
|
|
47
|
+
* between the CFG harvest (1-based) and the resolved reference site is fragile,
|
|
48
|
+
* while the callee identity is exact. It then applies the callee's summary at
|
|
49
|
+
* port `param argIndex`. This is the TITO ("taint-in-taint-out") propagation
|
|
50
|
+
* edge — a param laundered into a callee, the callee's behaviour deciding what
|
|
51
|
+
* happens next.
|
|
52
|
+
*
|
|
53
|
+
* `calleeName` is the site's dotted-callee tail (best-effort); absent when the
|
|
54
|
+
* callee chain was not statically resolvable, in which case the solver
|
|
55
|
+
* conservatively matches every outgoing call (sound over-approximation).
|
|
56
|
+
* `callLine` is the 1-based statement line as harvested (`StatementFacts.line`)
|
|
57
|
+
* — carried for hop display and as a TIE-BREAKER among several same-named
|
|
58
|
+
* callees of one caller, never as the primary join key.
|
|
59
|
+
*/
|
|
60
|
+
export interface ParamToCallArg {
|
|
61
|
+
readonly param: ParamIndex;
|
|
62
|
+
readonly callLine: number;
|
|
63
|
+
readonly argIndex: number;
|
|
64
|
+
readonly calleeName?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Sink kinds neutralised on EVERY harvested path from the param to this call
|
|
67
|
+
* argument (intersection-over-paths, #2084 review P1-2). A sanitizer between
|
|
68
|
+
* the param and the callee arg (`relay(x){ const y=escape(x); sinkFn(y); }`)
|
|
69
|
+
* must carry across the boundary so the callee's `paramToSink` of a
|
|
70
|
+
* neutralised kind does not fire (the cross-function false positive). Absent
|
|
71
|
+
* means none neutralised.
|
|
72
|
+
*/
|
|
73
|
+
readonly neutralized?: readonly SinkKind[];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* `param i` flows to the function's return value (a `return <expr>` use).
|
|
77
|
+
*
|
|
78
|
+
* RESERVED — not yet consumed by the fixpoint (#2084 review P1-1). The M3
|
|
79
|
+
* statement-level floor already treats every call as propagate-through, so it
|
|
80
|
+
* taints a callee's RESULT whenever the caller passes tainted input; param→
|
|
81
|
+
* return recall is therefore already covered, and consuming `paramToReturn`
|
|
82
|
+
* would only add PRECISION (avoiding the floor's over-approximation for
|
|
83
|
+
* functions that don't actually return their param) — a larger refactor
|
|
84
|
+
* deferred. Harvested + version-stamped so the precision pass can land without
|
|
85
|
+
* a cache-namespace bump.
|
|
86
|
+
*/
|
|
87
|
+
export interface ParamToReturn {
|
|
88
|
+
readonly param: ParamIndex;
|
|
89
|
+
/** Sink kinds neutralised on EVERY path param→return (intersection). */
|
|
90
|
+
readonly neutralized?: readonly SinkKind[];
|
|
91
|
+
}
|
|
92
|
+
/** `param i` reaches a modelled sink of kind `sinkKind` inside this function. */
|
|
93
|
+
export interface ParamToSink {
|
|
94
|
+
readonly param: ParamIndex;
|
|
95
|
+
readonly sinkKind: SinkKind;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The function itself GENERATES a source (a modelled source read, e.g.
|
|
99
|
+
* `req.body`) that reaches its return value — calling it yields tainted data
|
|
100
|
+
* with no tainted input required. The generative analogue of Pysa's
|
|
101
|
+
* `TaintSource[...]` return model. CONSUMED by the fixpoint via the caller's
|
|
102
|
+
* {@link CallResult} edges (#2084 review P1-1): a caller that uses such a
|
|
103
|
+
* function's result composes this into a finding/propagation. This is the
|
|
104
|
+
* genuinely-additive recall the floor cannot cover (the source is inside the
|
|
105
|
+
* callee — the caller passes no tainted input for the floor to propagate).
|
|
106
|
+
*/
|
|
107
|
+
export interface SourceToReturn {
|
|
108
|
+
readonly sourceKind: SourceKind;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* What a user-function call's RESULT flows into, in the CALLER (#2084 review
|
|
112
|
+
* P1-1). Recorded when a call to a (potentially generative) user function has
|
|
113
|
+
* its return value used by the caller. The fixpoint composes it with the
|
|
114
|
+
* callee's {@link SourceToReturn}: if the callee returns a generated source,
|
|
115
|
+
* the caller's downstream use of the result is tainted.
|
|
116
|
+
*/
|
|
117
|
+
export type CallResultDest = {
|
|
118
|
+
readonly to: 'sink';
|
|
119
|
+
readonly sinkKind: SinkKind;
|
|
120
|
+
} | {
|
|
121
|
+
readonly to: 'return';
|
|
122
|
+
} | {
|
|
123
|
+
readonly to: 'callArg';
|
|
124
|
+
readonly toCallee?: string;
|
|
125
|
+
readonly argIndex: ParamIndex;
|
|
126
|
+
};
|
|
127
|
+
/** The result of a call to `calleeName` flows to `dest` in this function. */
|
|
128
|
+
export interface CallResult {
|
|
129
|
+
readonly calleeName: string;
|
|
130
|
+
readonly dest: CallResultDest;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* A modelled source generated in this function flows into argument `argIndex`
|
|
134
|
+
* of a call at `callLine`. This SEEDS the interprocedural fixpoint: the source
|
|
135
|
+
* taints the callee's parameter, which the callee's summary then carries to a
|
|
136
|
+
* sink (one or more hops away). The cross-function analogue of an intra-
|
|
137
|
+
* procedural `source → sink` partial flow whose sink lives in the callee.
|
|
138
|
+
*/
|
|
139
|
+
export interface SourceToCallArg {
|
|
140
|
+
readonly sourceKind: SourceKind;
|
|
141
|
+
/** Carried for hop display + same-name tie-break; NOT the join key (see
|
|
142
|
+
* {@link ParamToCallArg} — the solver joins by `calleeName`). */
|
|
143
|
+
readonly callLine: number;
|
|
144
|
+
readonly argIndex: number;
|
|
145
|
+
readonly calleeName?: string;
|
|
146
|
+
/** Sink kinds neutralised on EVERY path from the generated source to this
|
|
147
|
+
* call argument (intersection; #2084 review P1-2 — see {@link ParamToCallArg}). */
|
|
148
|
+
readonly neutralized?: readonly SinkKind[];
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The compact taint abstraction of one function. All arrays are deterministically
|
|
152
|
+
* sorted by the harvester and deduped, so two structurally-equal summaries
|
|
153
|
+
* serialise identically (the {@link summaryVersion} contract).
|
|
154
|
+
*/
|
|
155
|
+
export interface FunctionSummary {
|
|
156
|
+
/** The resolved `Function`/`Method` graph node id this summary describes. */
|
|
157
|
+
readonly fnId: string;
|
|
158
|
+
/** Repo-relative source path (carried for diagnostics + the join debug). */
|
|
159
|
+
readonly filePath: string;
|
|
160
|
+
/** 1-based function start line (mirrors `FunctionCfg.functionStartLine`). */
|
|
161
|
+
readonly startLine: number;
|
|
162
|
+
/** Number of declared formal parameters (port arity). */
|
|
163
|
+
readonly paramCount: number;
|
|
164
|
+
/** param→return TITO edges. */
|
|
165
|
+
readonly paramToReturn: readonly ParamToReturn[];
|
|
166
|
+
/** param→callee-arg TITO edges (composed across `CALLS` in the fixpoint). */
|
|
167
|
+
readonly paramToCallArg: readonly ParamToCallArg[];
|
|
168
|
+
/** param→sink partial flows (a source reaching this param triggers a finding). */
|
|
169
|
+
readonly paramToSink: readonly ParamToSink[];
|
|
170
|
+
/** Generative source→return models. */
|
|
171
|
+
readonly sourceToReturn: readonly SourceToReturn[];
|
|
172
|
+
/** Generative source→callee-arg seeds (fixpoint entry points). */
|
|
173
|
+
readonly sourceToCallArg: readonly SourceToCallArg[];
|
|
174
|
+
/** Caller-side call-result flows — compose with callee `sourceToReturn`. */
|
|
175
|
+
readonly callResults: readonly CallResult[];
|
|
176
|
+
/**
|
|
177
|
+
* Content version stamp — `hash(own-facts ∪ sorted callee versions)`. The
|
|
178
|
+
* incremental cache key (Infer's content-keyed summary): equal across two
|
|
179
|
+
* runs iff the function's own taint facts AND every callee summary it depends
|
|
180
|
+
* on are unchanged. NOTE (#2084 review P1-1): callee-version composition is
|
|
181
|
+
* RESERVED — the harvester stamps the own-facts portion only
|
|
182
|
+
* ({@link ownFactsDigest}); the fixpoint does not yet recompose it.
|
|
183
|
+
*/
|
|
184
|
+
readonly version: string;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Deterministic digest of a summary's OWN taint facts (everything except
|
|
188
|
+
* `version`, which is derived). Order-independent within each edge category —
|
|
189
|
+
* the harvester already sorts, but the digest re-canonicalises so a reordering
|
|
190
|
+
* never changes the stamp. Used as the leaf of {@link summaryVersion}.
|
|
191
|
+
*/
|
|
192
|
+
export declare function ownFactsDigest(s: Pick<FunctionSummary, 'paramCount' | 'paramToReturn' | 'paramToCallArg' | 'paramToSink' | 'sourceToReturn' | 'sourceToCallArg' | 'callResults'>): string;
|
|
193
|
+
/**
|
|
194
|
+
* Content version stamp for a summary: `hash(ownFactsDigest ∪ sorted callee
|
|
195
|
+
* versions)`. Order-independent over callee versions (sorted). Equal iff the
|
|
196
|
+
* function's own facts AND every callee dependency are unchanged — this is the
|
|
197
|
+
* incremental invalidation primitive (a changed callee changes its version,
|
|
198
|
+
* which changes every transitive caller's version).
|
|
199
|
+
*/
|
|
200
|
+
export declare function summaryVersion(ownDigest: string, calleeVersions: readonly string[]): string;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-function taint SUMMARY model (#2084 M4 U2).
|
|
3
|
+
*
|
|
4
|
+
* A {@link FunctionSummary} is the compact, context-insensitive abstraction of
|
|
5
|
+
* one function's taint behaviour — the input to the interprocedural fixpoint
|
|
6
|
+
* (`interproc-solver.ts`). It is the GitNexus analogue of Pysa's `.pysa`
|
|
7
|
+
* models, Mariana Trench's "propagations", and CodeQL Models-as-Data summary
|
|
8
|
+
* rows: a function is reduced to how taint enters (params / generated sources),
|
|
9
|
+
* how it moves through (param→return, param→callee-arg), and where it lands
|
|
10
|
+
* (param→sink). The fixpoint composes these across resolved `CALLS` edges so a
|
|
11
|
+
* source in one function reaches a sink in another.
|
|
12
|
+
*
|
|
13
|
+
* ## Why summaries (not whole-program IFDS)
|
|
14
|
+
*
|
|
15
|
+
* The functional/summary method (Sharir-Pnueli 1981) analyses each function
|
|
16
|
+
* ONCE and propagates the result over the call graph — the same shape Pysa,
|
|
17
|
+
* Mariana Trench, and Infer use in production. GitNexus already resolves the
|
|
18
|
+
* call graph (`CALLS` edges carry final node ids), so the summary IS the only
|
|
19
|
+
* new artifact; propagation is graph reachability over a finite lattice.
|
|
20
|
+
*
|
|
21
|
+
* ## Granularity (first cut)
|
|
22
|
+
*
|
|
23
|
+
* WHOLE-PARAMETER. Ports are `param i`, `return`, and `receiver` — no field
|
|
24
|
+
* access paths (`arg0.field.sub`). Field sensitivity, callback-parameter ports
|
|
25
|
+
* (`Argument[0].Parameter[0]`), and context sensitivity are deferred (plan
|
|
26
|
+
* KTD; the largest JS/TS FN class — closures — stays a documented gap).
|
|
27
|
+
*
|
|
28
|
+
* ## Plain-data discipline
|
|
29
|
+
*
|
|
30
|
+
* A summary is a JSON-plain value type (no functions, class instances, Maps, or
|
|
31
|
+
* Symbols) so it survives `RunScopeResolutionStats` → `ScopeResolutionOutput`
|
|
32
|
+
* threading and any future worker/cache boundary unchanged — the same
|
|
33
|
+
* `Cloneable` constraint the CFG side channel obeys.
|
|
34
|
+
*/
|
|
35
|
+
/** Stable FNV-1a 32-bit hash → 8-char hex. Pure, deterministic, no deps. */
|
|
36
|
+
function fnv1a(input) {
|
|
37
|
+
let h = 0x811c9dc5;
|
|
38
|
+
for (let i = 0; i < input.length; i++) {
|
|
39
|
+
h ^= input.charCodeAt(i);
|
|
40
|
+
// 32-bit FNV prime multiply via shifts (avoids BigInt; stays in int32 land).
|
|
41
|
+
h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;
|
|
42
|
+
}
|
|
43
|
+
return (h >>> 0).toString(16).padStart(8, '0');
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Deterministic digest of a summary's OWN taint facts (everything except
|
|
47
|
+
* `version`, which is derived). Order-independent within each edge category —
|
|
48
|
+
* the harvester already sorts, but the digest re-canonicalises so a reordering
|
|
49
|
+
* never changes the stamp. Used as the leaf of {@link summaryVersion}.
|
|
50
|
+
*/
|
|
51
|
+
export function ownFactsDigest(s) {
|
|
52
|
+
const parts = [`p${s.paramCount}`];
|
|
53
|
+
parts.push(...s.paramToReturn
|
|
54
|
+
.map((r) => `r:${r.param}:${[...(r.neutralized ?? [])].sort().join(',')}`)
|
|
55
|
+
.sort());
|
|
56
|
+
parts.push(...s.paramToCallArg
|
|
57
|
+
.map((c) => `c:${c.param}:${c.callLine}:${c.argIndex}:${c.calleeName ?? ''}:${[...(c.neutralized ?? [])].sort().join(',')}`)
|
|
58
|
+
.sort());
|
|
59
|
+
parts.push(...s.paramToSink.map((k) => `k:${k.param}:${k.sinkKind}`).sort());
|
|
60
|
+
parts.push(...s.sourceToReturn.map((g) => `g:${g.sourceKind}`).sort());
|
|
61
|
+
parts.push(...s.sourceToCallArg
|
|
62
|
+
.map((g) => `s:${g.sourceKind}:${g.callLine}:${g.argIndex}:${g.calleeName ?? ''}:${[...(g.neutralized ?? [])].sort().join(',')}`)
|
|
63
|
+
.sort());
|
|
64
|
+
parts.push(...s.callResults
|
|
65
|
+
.map((cr) => {
|
|
66
|
+
const d = cr.dest;
|
|
67
|
+
const dest = d.to === 'sink'
|
|
68
|
+
? `sink:${d.sinkKind}`
|
|
69
|
+
: d.to === 'return'
|
|
70
|
+
? 'return'
|
|
71
|
+
: `arg:${d.toCallee ?? ''}:${d.argIndex}`;
|
|
72
|
+
return `cr:${cr.calleeName}:${dest}`;
|
|
73
|
+
})
|
|
74
|
+
.sort());
|
|
75
|
+
return fnv1a(parts.join('|'));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Content version stamp for a summary: `hash(ownFactsDigest ∪ sorted callee
|
|
79
|
+
* versions)`. Order-independent over callee versions (sorted). Equal iff the
|
|
80
|
+
* function's own facts AND every callee dependency are unchanged — this is the
|
|
81
|
+
* incremental invalidation primitive (a changed callee changes its version,
|
|
82
|
+
* which changes every transitive caller's version).
|
|
83
|
+
*/
|
|
84
|
+
export function summaryVersion(ownDigest, calleeVersions) {
|
|
85
|
+
return fnv1a(`${ownDigest}#${[...calleeVersions].sort().join(',')}`);
|
|
86
|
+
}
|
|
@@ -206,6 +206,24 @@ export declare const queryImporters: (targetFilePath: string) => Promise<string[
|
|
|
206
206
|
export declare const deleteAllCommunitiesAndProcesses: () => Promise<{
|
|
207
207
|
nodesDeleted: number;
|
|
208
208
|
}>;
|
|
209
|
+
/**
|
|
210
|
+
* Drop every interprocedural `TAINT_PATH` relationship (#2084 M4 U6). Used at
|
|
211
|
+
* the start of an incremental `--pdg` writeback so the `taintSummaries` phase
|
|
212
|
+
* re-materialises them from scratch on the FULL recomputed graph.
|
|
213
|
+
*
|
|
214
|
+
* TAINT_PATH validity is a WHOLE-PROGRAM property (a flow A→C can be
|
|
215
|
+
* invalidated by a change to an INTERMEDIATE function whose file is neither A
|
|
216
|
+
* nor C). The endpoint-writability extract rule (`extractChangedSubgraph`)
|
|
217
|
+
* cannot see that — an A→C edge between two unchanged files would be skipped
|
|
218
|
+
* and a stale finding would survive. So, exactly like Community/Process, the
|
|
219
|
+
* sound move is delete-all-then-rebuild: cheap because TAINT_PATH is sparse
|
|
220
|
+
* (per-run capped), and the compute side already rebuilds every summary each
|
|
221
|
+
* run. Relationship-level (TAINT_PATH is an edge type, not a node label), so a
|
|
222
|
+
* plain DELETE on the typed CodeRelation rows — endpoints are untouched.
|
|
223
|
+
*/
|
|
224
|
+
export declare const deleteAllInterprocTaintPaths: () => Promise<{
|
|
225
|
+
edgesDeleted: number;
|
|
226
|
+
}>;
|
|
209
227
|
/**
|
|
210
228
|
* Load the FTS extension on the supplied connection (or the singleton
|
|
211
229
|
* writable connection when none is given).
|
|
@@ -1629,6 +1629,61 @@ export const deleteAllCommunitiesAndProcesses = async () => {
|
|
|
1629
1629
|
}
|
|
1630
1630
|
return { nodesDeleted };
|
|
1631
1631
|
};
|
|
1632
|
+
/**
|
|
1633
|
+
* Drop every interprocedural `TAINT_PATH` relationship (#2084 M4 U6). Used at
|
|
1634
|
+
* the start of an incremental `--pdg` writeback so the `taintSummaries` phase
|
|
1635
|
+
* re-materialises them from scratch on the FULL recomputed graph.
|
|
1636
|
+
*
|
|
1637
|
+
* TAINT_PATH validity is a WHOLE-PROGRAM property (a flow A→C can be
|
|
1638
|
+
* invalidated by a change to an INTERMEDIATE function whose file is neither A
|
|
1639
|
+
* nor C). The endpoint-writability extract rule (`extractChangedSubgraph`)
|
|
1640
|
+
* cannot see that — an A→C edge between two unchanged files would be skipped
|
|
1641
|
+
* and a stale finding would survive. So, exactly like Community/Process, the
|
|
1642
|
+
* sound move is delete-all-then-rebuild: cheap because TAINT_PATH is sparse
|
|
1643
|
+
* (per-run capped), and the compute side already rebuilds every summary each
|
|
1644
|
+
* run. Relationship-level (TAINT_PATH is an edge type, not a node label), so a
|
|
1645
|
+
* plain DELETE on the typed CodeRelation rows — endpoints are untouched.
|
|
1646
|
+
*/
|
|
1647
|
+
export const deleteAllInterprocTaintPaths = async () => {
|
|
1648
|
+
if (!conn) {
|
|
1649
|
+
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
|
1650
|
+
}
|
|
1651
|
+
let edgesDeleted = 0;
|
|
1652
|
+
let countResult;
|
|
1653
|
+
try {
|
|
1654
|
+
countResult = await conn.query(`MATCH ()-[r:CodeRelation]->() WHERE r.type = 'TAINT_PATH' RETURN count(r) AS cnt`);
|
|
1655
|
+
const result = Array.isArray(countResult) ? countResult[0] : countResult;
|
|
1656
|
+
const rows = await result.getAll();
|
|
1657
|
+
const count = Number(rows[0]?.cnt ?? rows[0]?.[0] ?? 0);
|
|
1658
|
+
if (count > 0) {
|
|
1659
|
+
await conn.query(`MATCH ()-[r:CodeRelation]->() WHERE r.type = 'TAINT_PATH' DELETE r`);
|
|
1660
|
+
edgesDeleted = count;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
catch (err) {
|
|
1664
|
+
// A missing table on a freshly-initialized DB is the benign, expected case
|
|
1665
|
+
// (the count query above is what throws) — stay silent. Any OTHER failure
|
|
1666
|
+
// (lock, disk, native error) would leave stale TAINT_PATH rows that the
|
|
1667
|
+
// subsequent re-extract then DUPLICATES (CodeRelation has no PK), so it
|
|
1668
|
+
// must ABORT the writeback (#2084 review P2-5): re-throw so the caller's
|
|
1669
|
+
// crash-recovery dirty flag forces a clean full rebuild on the next run,
|
|
1670
|
+
// rather than silently writing duplicate cross-function findings.
|
|
1671
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1672
|
+
if (/no table|not exist|not found|does not exist|Table .* does not exist/i.test(msg)) {
|
|
1673
|
+
if (countResult)
|
|
1674
|
+
await closeQueryResults(countResult);
|
|
1675
|
+
return { edgesDeleted };
|
|
1676
|
+
}
|
|
1677
|
+
if (countResult)
|
|
1678
|
+
await closeQueryResults(countResult);
|
|
1679
|
+
throw new Error(`[taint-interproc] failed to clear existing TAINT_PATH edges before incremental ` +
|
|
1680
|
+
`re-write (${msg}) — aborting to avoid duplicate cross-function findings; ` +
|
|
1681
|
+
`the next run will full-rebuild`);
|
|
1682
|
+
}
|
|
1683
|
+
if (countResult)
|
|
1684
|
+
await closeQueryResults(countResult);
|
|
1685
|
+
return { edgesDeleted };
|
|
1686
|
+
};
|
|
1632
1687
|
// ============================================================================
|
|
1633
1688
|
// Full-Text Search (FTS) Functions
|
|
1634
1689
|
// ============================================================================
|
|
@@ -72,6 +72,12 @@ export interface AnalyzeOptions {
|
|
|
72
72
|
/** Per-finding taint hop cap (#2083 M3, KTD6). Forwarded to
|
|
73
73
|
* `PipelineOptions.pdgMaxTaintHops`. No CLI flag or rc key (KTD8). */
|
|
74
74
|
pdgMaxTaintHops?: number;
|
|
75
|
+
/** Per-run cross-function findings/hops/edges caps (#2084 review P1-3).
|
|
76
|
+
* Forwarded to the matching `PipelineOptions.pdgMaxInterproc*`; resolved
|
|
77
|
+
* into `RepoMeta.pdg`. No CLI flag or rc key (KTD8). */
|
|
78
|
+
pdgMaxInterprocFindings?: number;
|
|
79
|
+
pdgMaxInterprocHops?: number;
|
|
80
|
+
pdgMaxInterprocEdges?: number;
|
|
75
81
|
/**
|
|
76
82
|
* Default branch threaded into generated AGENTS.md / CLAUDE.md so the
|
|
77
83
|
* regression-compare example uses the configured branch instead of a
|
|
@@ -192,7 +198,7 @@ export declare const collectBranchCacheKeys: (storagePath: string, excludeDir?:
|
|
|
192
198
|
* defaults so an explicit-default run compares equal to a default run
|
|
193
199
|
* (`0` = unlimited is preserved as `0`). Pure + exported for testing.
|
|
194
200
|
*/
|
|
195
|
-
type PdgOptions = Pick<AnalyzeOptions, 'pdg' | 'pdgMaxFunctionLines' | 'pdgMaxEdgesPerFunction' | 'pdgMaxReachingDefEdgesPerFunction' | 'pdgMaxTaintFindingsPerFunction' | 'pdgMaxTaintHops'>;
|
|
201
|
+
type PdgOptions = Pick<AnalyzeOptions, 'pdg' | 'pdgMaxFunctionLines' | 'pdgMaxEdgesPerFunction' | 'pdgMaxReachingDefEdgesPerFunction' | 'pdgMaxTaintFindingsPerFunction' | 'pdgMaxTaintHops' | 'pdgMaxInterprocFindings' | 'pdgMaxInterprocHops' | 'pdgMaxInterprocEdges'>;
|
|
196
202
|
export declare const resolvePdgConfig: (options: PdgOptions) => RepoMeta["pdg"];
|
|
197
203
|
/**
|
|
198
204
|
* Whether the requested `--pdg` configuration differs from the one the
|
package/dist/core/run-analyze.js
CHANGED
|
@@ -13,7 +13,7 @@ import fs from 'fs/promises';
|
|
|
13
13
|
import { execFileSync } from 'child_process';
|
|
14
14
|
import { runPipelineFromRepo } from './ingestion/pipeline.js';
|
|
15
15
|
import { resetDegradedParseCounter } from './tree-sitter/safe-parse.js';
|
|
16
|
-
import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, loadCachedEmbeddings, deleteNodesForFile, deleteAllCommunitiesAndProcesses, queryImporters, loadFTSExtension, } from './lbug/lbug-adapter.js';
|
|
16
|
+
import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, loadCachedEmbeddings, deleteNodesForFile, deleteAllCommunitiesAndProcesses, deleteAllInterprocTaintPaths, queryImporters, loadFTSExtension, } from './lbug/lbug-adapter.js';
|
|
17
17
|
import { createSearchFTSIndexes, verifySearchFTSIndexes } from './search/fts-indexes.js';
|
|
18
18
|
import { resolveAnalyzeInstallPolicy } from './lbug/extension-loader.js';
|
|
19
19
|
import { startWalCheckpointDriver, } from './lbug/wal-checkpoint-driver.js';
|
|
@@ -21,6 +21,8 @@ import { getStoragePaths, resolveBranchPlacement, saveMeta, loadMeta, ensureGitN
|
|
|
21
21
|
import { DEFAULT_PDG_MAX_FUNCTION_LINES } from './ingestion/cfg/collect.js';
|
|
22
22
|
import { DEFAULT_MAX_CFG_EDGES_PER_FUNCTION, DEFAULT_PDG_MAX_REACHING_DEF_EDGES_PER_FUNCTION, } from './ingestion/cfg/emit.js';
|
|
23
23
|
import { DEFAULT_PDG_MAX_TAINT_FINDINGS_PER_FUNCTION, DEFAULT_PDG_MAX_TAINT_HOPS, } from './ingestion/taint/propagate.js';
|
|
24
|
+
import { DEFAULT_MAX_INTERPROC_HOPS, DEFAULT_PDG_MAX_INTERPROC_FINDINGS, } from './ingestion/taint/interproc-solver.js';
|
|
25
|
+
import { DEFAULT_PDG_MAX_INTERPROC_EDGES } from './ingestion/taint/interproc-emit.js';
|
|
24
26
|
import { taintModelVersion } from './ingestion/taint/typescript-model.js';
|
|
25
27
|
import { computeFileHashes, diffFileHashes } from '../storage/file-hash.js';
|
|
26
28
|
import { extractChangedSubgraph, computeEffectiveWriteSet, } from './incremental/subgraph-extract.js';
|
|
@@ -144,6 +146,12 @@ export const resolvePdgConfig = (options) => options.pdg === true
|
|
|
144
146
|
// writeback that populates TAINTED/SANITIZES rows without `--force`.
|
|
145
147
|
maxTaintFindingsPerFunction: options.pdgMaxTaintFindingsPerFunction ?? DEFAULT_PDG_MAX_TAINT_FINDINGS_PER_FUNCTION,
|
|
146
148
|
maxTaintHops: options.pdgMaxTaintHops ?? DEFAULT_PDG_MAX_TAINT_HOPS,
|
|
149
|
+
// #2084 review P1-3: cross-function caps. Absent on an M3-era stamp →
|
|
150
|
+
// pdgModeMismatch trips the first run that adds them (key-union),
|
|
151
|
+
// forcing the full writeback that re-materialises TAINT_PATH bounded.
|
|
152
|
+
maxInterprocFindings: options.pdgMaxInterprocFindings ?? DEFAULT_PDG_MAX_INTERPROC_FINDINGS,
|
|
153
|
+
maxInterprocHops: options.pdgMaxInterprocHops ?? DEFAULT_MAX_INTERPROC_HOPS,
|
|
154
|
+
maxInterprocEdges: options.pdgMaxInterprocEdges ?? DEFAULT_PDG_MAX_INTERPROC_EDGES,
|
|
147
155
|
// Built-in model digest (KTD7/R7): persisted findings must never
|
|
148
156
|
// outlive the model that produced them — ANY model-content change
|
|
149
157
|
// ships as a new digest and repopulates the taint edges.
|
|
@@ -501,6 +509,9 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
501
509
|
pdgMaxReachingDefEdgesPerFunction: options.pdgMaxReachingDefEdgesPerFunction,
|
|
502
510
|
pdgMaxTaintFindingsPerFunction: options.pdgMaxTaintFindingsPerFunction,
|
|
503
511
|
pdgMaxTaintHops: options.pdgMaxTaintHops,
|
|
512
|
+
pdgMaxInterprocFindings: options.pdgMaxInterprocFindings,
|
|
513
|
+
pdgMaxInterprocHops: options.pdgMaxInterprocHops,
|
|
514
|
+
pdgMaxInterprocEdges: options.pdgMaxInterprocEdges,
|
|
504
515
|
fetchWrappers: options.fetchWrappers,
|
|
505
516
|
});
|
|
506
517
|
// ── Phase 2: LadybugDB (60–85%) ──────────────────────────────────
|
|
@@ -704,6 +715,15 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
|
|
|
704
715
|
// from the fresh pipeline output below. Required for the
|
|
705
716
|
// "Leiden runs on the FULL graph" correctness invariant.
|
|
706
717
|
await deleteAllCommunitiesAndProcesses();
|
|
718
|
+
// 2b. Drop interprocedural TAINT_PATH edges (#2084 M4 U6) when pdg is on
|
|
719
|
+
// — their validity is a whole-program property (an A→C flow can be
|
|
720
|
+
// invalidated by a change to an intermediate function on a third
|
|
721
|
+
// file), so endpoint-writability extraction can't refresh them.
|
|
722
|
+
// extractChangedSubgraph re-includes all of them from the fresh
|
|
723
|
+
// graph (isGraphWideRelType), mirroring Community/Process.
|
|
724
|
+
if (options.pdg === true) {
|
|
725
|
+
await deleteAllInterprocTaintPaths();
|
|
726
|
+
}
|
|
707
727
|
// 3. Extract the changed subgraph from the FULL ctx.graph and write
|
|
708
728
|
// only that. Unchanged-file rows in the DB stay untouched. Pass
|
|
709
729
|
// the SAME effectiveWriteSet so the subgraph and the deletes
|
|
@@ -2425,16 +2425,81 @@ export class LocalBackend {
|
|
|
2425
2425
|
}
|
|
2426
2426
|
}
|
|
2427
2427
|
const { rows, totalFindings } = await runAnchoredQuery();
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2428
|
+
// M4 (#2084 U7): cross-function findings ride TAINT_PATH edges (Function/
|
|
2429
|
+
// Method → Function/Method), separate from the intra-procedural TAINTED
|
|
2430
|
+
// BasicBlock rows above. Enumerate them too so `explain` is the discovery
|
|
2431
|
+
// surface for interprocedural flows (TAINT_PATH stays out of
|
|
2432
|
+
// VALID_RELATION_TYPES + the web schema, like TAINTED). File-anchored:
|
|
2433
|
+
// filter on the source function's file; symbol-anchored: either endpoint
|
|
2434
|
+
// matches the symbol name; anchorless: all (bounded by LIMIT). Computed
|
|
2435
|
+
// BEFORE the no-taint early returns — a repo with ONLY cross-function
|
|
2436
|
+
// findings (no intra-procedural TAINTED rows) must not look empty.
|
|
2437
|
+
const runInterprocQuery = async () => {
|
|
2438
|
+
const where = [`r.type = 'TAINT_PATH'`];
|
|
2439
|
+
const p = {};
|
|
2440
|
+
if (anchor?.symbol) {
|
|
2441
|
+
where.push('(a.name = $ipSym OR b.name = $ipSym)');
|
|
2442
|
+
p.ipSym = anchor.symbol;
|
|
2443
|
+
}
|
|
2444
|
+
else if (anchor?.file) {
|
|
2445
|
+
// Match EITHER endpoint's file — a cross-function flow anchored on the
|
|
2446
|
+
// SINK's file (b) is as relevant as one anchored on the source's (a).
|
|
2447
|
+
where.push('(a.filePath = $ipFile OR a.filePath ENDS WITH $ipSuffix OR ' +
|
|
2448
|
+
'b.filePath = $ipFile OR b.filePath ENDS WITH $ipSuffix)');
|
|
2449
|
+
p.ipFile = anchor.file;
|
|
2450
|
+
p.ipSuffix = `/${anchor.file}`;
|
|
2451
|
+
}
|
|
2452
|
+
const matchClause = `MATCH (a)-[r:CodeRelation]->(b)\n WHERE ${where.join(' AND ')}`;
|
|
2453
|
+
// Page query + a separate COUNT (#2084 review P2-4): the page is
|
|
2454
|
+
// LIMIT-capped, so its row count cannot stand in for the true total —
|
|
2455
|
+
// run a COUNT with the same WHERE (no LIMIT) like the intra layer does.
|
|
2456
|
+
const [ipRows, ipCountRows] = await Promise.all([
|
|
2457
|
+
executeParameterized(repo.lbugPath, `${matchClause}
|
|
2458
|
+
RETURN a.filePath AS file, a.name AS sourceFn, a.startLine AS sourceLine,
|
|
2459
|
+
b.name AS sinkFn, b.startLine AS sinkLine, r.reason AS reason
|
|
2460
|
+
ORDER BY sourceFn, sinkFn, reason
|
|
2461
|
+
LIMIT ${limit}`, p),
|
|
2462
|
+
executeParameterized(repo.lbugPath, `${matchClause}\n RETURN COUNT(*) AS total`, p),
|
|
2463
|
+
]);
|
|
2464
|
+
const total = Number(ipCountRows[0]?.total ?? ipCountRows[0]?.[0] ?? 0);
|
|
2465
|
+
const findings = ipRows.map((r) => {
|
|
2466
|
+
const decoded = decodeTaintPath(r.reason ?? r[5]);
|
|
2467
|
+
const hops = decoded.ok
|
|
2468
|
+
? decoded.hops.map((h) => ({ function: h.variable, line: h.line }))
|
|
2469
|
+
: [];
|
|
2470
|
+
return {
|
|
2471
|
+
interprocedural: true,
|
|
2472
|
+
file: String(r.file ?? r[0] ?? ''),
|
|
2473
|
+
sinkKind: decoded.ok ? (decoded.kind ?? 'unknown') : 'unknown',
|
|
2474
|
+
source: { function: String(r.sourceFn ?? r[1] ?? ''), line: r.sourceLine ?? r[2] },
|
|
2475
|
+
sink: { function: String(r.sinkFn ?? r[3] ?? ''), line: r.sinkLine ?? r[4] },
|
|
2476
|
+
hops,
|
|
2477
|
+
...(decoded.ok && decoded.truncated ? { pathIncomplete: true } : {}),
|
|
2478
|
+
};
|
|
2479
|
+
});
|
|
2480
|
+
return { findings, total };
|
|
2481
|
+
};
|
|
2482
|
+
const { findings: interprocFindings, total: interprocTotal } = await runInterprocQuery();
|
|
2483
|
+
if (totalFindings === 0 &&
|
|
2484
|
+
interprocFindings.length === 0 &&
|
|
2485
|
+
pdgStamped === undefined &&
|
|
2486
|
+
!target) {
|
|
2487
|
+
// Meta was unreadable and the repo-wide enumerate (both layers) found
|
|
2488
|
+
// nothing — the counts above WERE the existence probe; surface the hint.
|
|
2431
2489
|
return { findings: [], totalFindings: 0, note: NO_TAINT_NOTE };
|
|
2432
2490
|
}
|
|
2433
|
-
if (totalFindings === 0 &&
|
|
2491
|
+
if (totalFindings === 0 &&
|
|
2492
|
+
interprocFindings.length === 0 &&
|
|
2493
|
+
pdgStamped === undefined &&
|
|
2494
|
+
target) {
|
|
2434
2495
|
// Anchored miss with unreadable meta: one extra bounded probe decides
|
|
2435
|
-
// "no findings for this anchor" vs "no taint layer at all".
|
|
2496
|
+
// "no findings for this anchor" vs "no taint layer at all". Probe BOTH
|
|
2497
|
+
// intra (TAINTED) and inter (TAINT_PATH) existence.
|
|
2436
2498
|
const probe = await executeParameterized(repo.lbugPath, `MATCH (a:BasicBlock)-[r:CodeRelation]->(b:BasicBlock) WHERE r.type = 'TAINTED' RETURN r.reason AS reason LIMIT 1`, {});
|
|
2437
|
-
|
|
2499
|
+
const ipProbe = probe.length === 0
|
|
2500
|
+
? await executeParameterized(repo.lbugPath, `MATCH (a)-[r:CodeRelation]->(b) WHERE r.type = 'TAINT_PATH' RETURN r.reason AS reason LIMIT 1`, {})
|
|
2501
|
+
: [];
|
|
2502
|
+
if (probe.length === 0 && ipProbe.length === 0) {
|
|
2438
2503
|
return { findings: [], totalFindings: 0, note: NO_TAINT_NOTE };
|
|
2439
2504
|
}
|
|
2440
2505
|
}
|
|
@@ -2479,12 +2544,30 @@ export class LocalBackend {
|
|
|
2479
2544
|
...(decoded.truncated ? { pathIncomplete: true } : {}),
|
|
2480
2545
|
};
|
|
2481
2546
|
});
|
|
2547
|
+
// Combine both layers and re-apply the page LIMIT to the union — each
|
|
2548
|
+
// layer was queried with its own LIMIT, so the union can hold up to 2×;
|
|
2549
|
+
// cap it so `findings.length` honours the caller's `limit`. `truncated`
|
|
2550
|
+
// reflects EITHER layer overflowing OR the union being trimmed here, and
|
|
2551
|
+
// `totalFindings` counts both layers' matched rows (the intra COUNT plus
|
|
2552
|
+
// the interproc rows returned — interproc has no separate COUNT, so a
|
|
2553
|
+
// capped interproc layer is reflected via `truncated`, never undercounted
|
|
2554
|
+
// into a false "complete" signal). Review: code-review #2/#4 (explain
|
|
2555
|
+
// accounting + sink-file anchoring) — both layers now accounted.
|
|
2556
|
+
const combined = [...findings, ...interprocFindings];
|
|
2557
|
+
const pageFindings = combined.length > limit ? combined.slice(0, limit) : combined;
|
|
2558
|
+
// Truncated iff EITHER layer overflowed its own LIMIT (strict `>` — exactly
|
|
2559
|
+
// `limit` rows is not truncated), OR the combined union was trimmed to the
|
|
2560
|
+
// page (#2084 review P2-4). `totalFindings` uses the interproc COUNT, not
|
|
2561
|
+
// the capped slice length, so it never undercounts.
|
|
2562
|
+
const truncated = totalFindings > findings.length ||
|
|
2563
|
+
interprocTotal > interprocFindings.length ||
|
|
2564
|
+
combined.length > pageFindings.length;
|
|
2482
2565
|
return {
|
|
2483
2566
|
...(anchor ? { anchor } : {}),
|
|
2484
|
-
findings,
|
|
2485
|
-
totalFindings,
|
|
2486
|
-
...(
|
|
2487
|
-
note: 'Intra-procedural
|
|
2567
|
+
findings: pageFindings,
|
|
2568
|
+
totalFindings: totalFindings + interprocTotal,
|
|
2569
|
+
...(truncated ? { truncated: true } : {}),
|
|
2570
|
+
note: 'Intra-procedural (TAINTED, statement hops) AND cross-function (TAINT_PATH, function hops, `interprocedural: true`) flows are modeled. Closure/callback, property/field, and implicit flows are NOT modeled; absence of a finding is not proof of safety. Cross-function findings are context-insensitive and may over-attribute among same-named callees. SANITIZES (kill) edges are queryable via cypher.',
|
|
2488
2571
|
};
|
|
2489
2572
|
}
|
|
2490
2573
|
/**
|
package/dist/mcp/tools.js
CHANGED
|
@@ -473,18 +473,19 @@ SERVICE: optional monorepo path prefix (case-sensitive path segments). When "rep
|
|
|
473
473
|
},
|
|
474
474
|
{
|
|
475
475
|
name: 'explain',
|
|
476
|
-
description: `Explain persisted taint findings
|
|
476
|
+
description: `Explain persisted taint findings recorded by \`gitnexus analyze --pdg\`: intra-procedural source→sink data flows (TAINTED edges, statement-level hops) AND cross-function flows (TAINT_PATH edges, function-level hops, marked \`interprocedural: true\`).
|
|
477
477
|
|
|
478
|
-
Each finding carries the sink category (command-injection, code-injection, path-traversal, sql-injection, xss)
|
|
478
|
+
Each finding carries the sink category (command-injection, code-injection, path-traversal, sql-injection, xss) and the ordered hop path. Intra-procedural findings carry source/sink lines and the variable on each hop; interprocedural findings carry the source and sink FUNCTION names and the chain of functions the taint crossed (decoded from the persisted path encoding).
|
|
479
479
|
|
|
480
480
|
WHEN TO USE: Security review — "what taint findings exist in this repo / file / function?". Requires the repo to be indexed with \`gitnexus analyze --pdg\`; without that layer the tool returns a clear "no taint layer" note, not an error.
|
|
481
481
|
|
|
482
482
|
ANCHORLESS (no "target"): enumerates all persisted findings for the repo — bounded ("limit", deterministic order), with "totalFindings" and a "truncated" flag.
|
|
483
|
-
ANCHORED ("target" = file path or symbol/function name): full hop detail for that anchor. A file-ish target (contains "/" or an extension) filters by file; a symbol name resolves like context() — ambiguous names return ranked candidates, unknown names return not-found. Symbol anchoring is line-range granular
|
|
483
|
+
ANCHORED ("target" = file path or symbol/function name): full hop detail for that anchor. A file-ish target (contains "/" or an extension) filters by file; a symbol name resolves like context() — ambiguous names return ranked candidates, unknown names return not-found. Symbol anchoring is line-range granular for intra-procedural findings; cross-function findings match when the symbol is the source OR sink function.
|
|
484
484
|
|
|
485
|
-
CONTRACT CAVEATS (
|
|
486
|
-
- Cross-function flows
|
|
487
|
-
-
|
|
485
|
+
CONTRACT CAVEATS (absent flows are NOT proof of safety):
|
|
486
|
+
- Cross-function flows ARE modeled (#2084 M4): a source flowing through helper functions into a sink is found, via summary composition over the call graph (context-insensitive — return/call-site merging is accepted).
|
|
487
|
+
- Cross-function matching is by callee NAME (context-insensitive): when one caller invokes two distinct same-named callees, a flow into one over-attributes to both — a cross-function finding does not prove the taint reached every same-named function (sound over-report, never a missed flow).
|
|
488
|
+
- Closure/callback flows are invisible in both directions (e.g. arr.forEach(() => sink(y))) — the largest false-negative class.
|
|
488
489
|
- Property/field flows are not tracked (obj.x = taint; sink(obj.y) has no chain).
|
|
489
490
|
- Guard-style sanitizers (if (isValid(x))) and implicit/control-dependence flows are not modeled.
|
|
490
491
|
- CommonJS aliasing is partially modeled (require('<literal>') joins resolve; dynamic requires do not).
|
|
@@ -145,6 +145,16 @@ export interface RepoMeta {
|
|
|
145
145
|
* bounds the persisted hop-encoded `reason`). Optional for the same
|
|
146
146
|
* M2-era-stamp upgrade reason as the findings cap. */
|
|
147
147
|
maxTaintHops?: number;
|
|
148
|
+
/**
|
|
149
|
+
* Per-run cross-function caps, resolved (0 = unlimited; #2084 M4 review
|
|
150
|
+
* P1-3). ABSENT on an M3-era stamp — that absence trips `pdgModeMismatch`
|
|
151
|
+
* on the first run that adds them and forces the full writeback that
|
|
152
|
+
* re-materialises TAINT_PATH within bounds. Optional for that upgrade
|
|
153
|
+
* reason; resolved (always present) on every post-fix write.
|
|
154
|
+
*/
|
|
155
|
+
maxInterprocFindings?: number;
|
|
156
|
+
maxInterprocHops?: number;
|
|
157
|
+
maxInterprocEdges?: number;
|
|
148
158
|
/**
|
|
149
159
|
* Digest of the built-in taint model the persisted findings were
|
|
150
160
|
* produced under (#2083 M3 KTD7/R7). Any model-content change ships a
|