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
|
@@ -24,7 +24,10 @@ const fs = require('fs');
|
|
|
24
24
|
const path = require('path');
|
|
25
25
|
const { spawnSync } = require('child_process');
|
|
26
26
|
const { acquireHookSlot } = require('./hook-lock.cjs');
|
|
27
|
-
const {
|
|
27
|
+
const {
|
|
28
|
+
hasGitNexusDbLockedByGitNexusServer,
|
|
29
|
+
resolveUnixGuardTimeout,
|
|
30
|
+
} = require('./hook-db-lock-probe.cjs');
|
|
28
31
|
const { formatAnalyzeCommand } = require('./resolve-analyze-cmd.cjs');
|
|
29
32
|
|
|
30
33
|
function readInput() {
|
|
@@ -198,10 +201,73 @@ function resolveCliPath() {
|
|
|
198
201
|
return cliPath;
|
|
199
202
|
}
|
|
200
203
|
|
|
204
|
+
// Debounce for the unguarded-CLI diagnostic below (#2163 follow-up review):
|
|
205
|
+
// at most one line per (short-lived) hook process, even if a future change
|
|
206
|
+
// runs the CLI more than once.
|
|
207
|
+
let unguardedCliWarned = false;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Unix orphan containment (#2163 follow-up): the augment CLI is the
|
|
211
|
+
* longest-lived hook child (inner spawnSync timeout 7s locally, 12s via
|
|
212
|
+
* npx), so on Unix it gets the same SIGKILL-surviving coreutils `timeout`
|
|
213
|
+
* wrapper as the probe's lsof/ps. The wrapper budget is ceil(inner/1000)+1
|
|
214
|
+
* seconds — STRICTLY greater than the inner spawnSync timeout, so on the
|
|
215
|
+
* supervised path Node's SIGTERM always fires first and the existing
|
|
216
|
+
* error/status contract is untouched. Once the hook itself has been
|
|
217
|
+
* SIGKILLed (exactly the orphan case the wrapper exists for), the guard
|
|
218
|
+
* semantics differ per branch:
|
|
219
|
+
* - direct exec (the CLI is the guard's CHILD): `-k 1` TERM-first — a
|
|
220
|
+
* SIGTERM-immune CLI can hold the guard ~1s past the inner timeout
|
|
221
|
+
* before the `-k` SIGKILL escalation reaps it.
|
|
222
|
+
* - npx (the CLI is a GRANDCHILD: guard → npx → CLI): `-s KILL` — the
|
|
223
|
+
* budget expiry SIGKILLs the whole process group outright. TERM-first
|
|
224
|
+
* would kill only the obedient npx parent, making `timeout` reap it and
|
|
225
|
+
* return before the `-k` escalation ever fires, stranding a
|
|
226
|
+
* SIGTERM-immune CLI grandchild unbounded (reproduced on coreutils
|
|
227
|
+
* 9.x). `-k 1` is retained alongside `-s KILL` as a harmless belt: with
|
|
228
|
+
* `-s KILL` the `-k` escalation signal is also KILL. Two residual gaps
|
|
229
|
+
* on this branch, both bounded by "no worse than pre-fix" (where the
|
|
230
|
+
* grandchild received no signal at all): the group-wide SIGKILL is
|
|
231
|
+
* coreutils semantics — a busybox `timeout` passes the self-test (it
|
|
232
|
+
* has `-k` and propagates exit status) but signals only its direct
|
|
233
|
+
* child, so a busybox guard cannot reach the grandchild; and on the
|
|
234
|
+
* SUPERVISED path (hook alive, inner spawnSync timeout SIGTERMs the
|
|
235
|
+
* guard) coreutils forwards TERM rather than the `-s` signal, npx dies,
|
|
236
|
+
* and the guard exits before any KILL fires — so a SIGTERM-immune CLI
|
|
237
|
+
* grandchild still escapes in those two cases.
|
|
238
|
+
* If the sibling probe predates the resolveUnixGuardTimeout export (version
|
|
239
|
+
* skew), the adapter degrades to the unwrapped invocation instead of
|
|
240
|
+
* throwing. Windows is deliberately NOT wrapped — there is no coreutils
|
|
241
|
+
* timeout to resolve there and the resolver's self-test spawns /bin/sh — so
|
|
242
|
+
* on win32 (the npx.cmd path) and whenever the guard resolves to null (e.g.
|
|
243
|
+
* macOS without Homebrew coreutils — reported once under GITNEXUS_DEBUG)
|
|
244
|
+
* the argv stays byte-identical to the pre-wrap invocation.
|
|
245
|
+
*/
|
|
201
246
|
function runGitNexusCli(cliPath, args, cwd, timeout) {
|
|
202
247
|
const isWin = process.platform === 'win32';
|
|
248
|
+
// Version-skew guard (#2163 follow-up review): an older sibling probe
|
|
249
|
+
// without the resolveUnixGuardTimeout export must degrade to the unwrapped
|
|
250
|
+
// invocation — a TypeError here would be swallowed by the caller's catch
|
|
251
|
+
// and silently kill the augment.
|
|
252
|
+
const guard =
|
|
253
|
+
isWin || typeof resolveUnixGuardTimeout !== 'function' ? null : resolveUnixGuardTimeout();
|
|
254
|
+
if (!isWin && !guard && !unguardedCliWarned && isDebugEnabled()) {
|
|
255
|
+
// Diagnose the "stays unwrapped" Unix paths once per hook process: no
|
|
256
|
+
// usable coreutils timeout/gtimeout (e.g. macOS without Homebrew
|
|
257
|
+
// coreutils), GITNEXUS_HOOK_TIMEOUT_PATH=disabled, or probe skew above.
|
|
258
|
+
unguardedCliWarned = true;
|
|
259
|
+
process.stderr.write(
|
|
260
|
+
'[GitNexus hook] no usable timeout/gtimeout guard; augment CLI child runs unguarded\n',
|
|
261
|
+
);
|
|
262
|
+
}
|
|
203
263
|
if (cliPath) {
|
|
204
|
-
|
|
264
|
+
const [cmd, cmdArgs] = guard
|
|
265
|
+
? [
|
|
266
|
+
guard,
|
|
267
|
+
['-k', '1', String(Math.ceil(timeout / 1000) + 1), process.execPath, cliPath, ...args],
|
|
268
|
+
]
|
|
269
|
+
: [process.execPath, [cliPath, ...args]];
|
|
270
|
+
return spawnSync(cmd, cmdArgs, {
|
|
205
271
|
encoding: 'utf-8',
|
|
206
272
|
timeout,
|
|
207
273
|
cwd,
|
|
@@ -209,7 +275,27 @@ function runGitNexusCli(cliPath, args, cwd, timeout) {
|
|
|
209
275
|
windowsHide: true,
|
|
210
276
|
});
|
|
211
277
|
}
|
|
212
|
-
|
|
278
|
+
// A non-null guard implies non-Windows, so the wrapped arm can hardcode
|
|
279
|
+
// plain `npx`. The wrapped arm leads with `-s KILL` (NOT TERM-first like
|
|
280
|
+
// the direct branch above): the CLI here is a grandchild behind npx — see
|
|
281
|
+
// the docblock.
|
|
282
|
+
const [cmd, cmdArgs] = guard
|
|
283
|
+
? [
|
|
284
|
+
guard,
|
|
285
|
+
[
|
|
286
|
+
'-s',
|
|
287
|
+
'KILL',
|
|
288
|
+
'-k',
|
|
289
|
+
'1',
|
|
290
|
+
String(Math.ceil((timeout + 5000) / 1000) + 1),
|
|
291
|
+
'npx',
|
|
292
|
+
'-y',
|
|
293
|
+
'gitnexus',
|
|
294
|
+
...args,
|
|
295
|
+
],
|
|
296
|
+
]
|
|
297
|
+
: [isWin ? 'npx.cmd' : 'npx', ['-y', 'gitnexus', ...args]];
|
|
298
|
+
return spawnSync(cmd, cmdArgs, {
|
|
213
299
|
encoding: 'utf-8',
|
|
214
300
|
timeout: timeout + 5000,
|
|
215
301
|
cwd,
|
|
@@ -15,7 +15,10 @@ const fs = require('fs');
|
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const { spawnSync } = require('child_process');
|
|
17
17
|
const { acquireHookSlot } = require('./hook-lock.cjs');
|
|
18
|
-
const {
|
|
18
|
+
const {
|
|
19
|
+
hasGitNexusDbLockedByGitNexusServer,
|
|
20
|
+
resolveUnixGuardTimeout,
|
|
21
|
+
} = require('./hook-db-lock-probe.cjs');
|
|
19
22
|
const { formatAnalyzeCommand } = require('./resolve-analyze-cmd.cjs');
|
|
20
23
|
|
|
21
24
|
/**
|
|
@@ -218,14 +221,76 @@ function resolveCliPath() {
|
|
|
218
221
|
return cliPath;
|
|
219
222
|
}
|
|
220
223
|
|
|
224
|
+
// Debounce for the unguarded-CLI diagnostic below (#2163 follow-up review):
|
|
225
|
+
// at most one line per (short-lived) hook process, even if a future change
|
|
226
|
+
// runs the CLI more than once.
|
|
227
|
+
let unguardedCliWarned = false;
|
|
228
|
+
|
|
221
229
|
/**
|
|
222
230
|
* Spawn a gitnexus CLI command synchronously.
|
|
223
231
|
* Returns the stderr output (KuzuDB captures stdout at OS level).
|
|
232
|
+
*
|
|
233
|
+
* Unix orphan containment (#2163 follow-up): the augment CLI is the
|
|
234
|
+
* longest-lived hook child (inner spawnSync timeout 7s locally, 12s via
|
|
235
|
+
* npx), so on Unix it gets the same SIGKILL-surviving coreutils `timeout`
|
|
236
|
+
* wrapper as the probe's lsof/ps. The wrapper budget is ceil(inner/1000)+1
|
|
237
|
+
* seconds — STRICTLY greater than the inner spawnSync timeout, so on the
|
|
238
|
+
* supervised path Node's SIGTERM always fires first and the existing
|
|
239
|
+
* error/status contract is untouched. Once the hook itself has been
|
|
240
|
+
* SIGKILLed (exactly the orphan case the wrapper exists for), the guard
|
|
241
|
+
* semantics differ per branch:
|
|
242
|
+
* - direct exec (the CLI is the guard's CHILD): `-k 1` TERM-first — a
|
|
243
|
+
* SIGTERM-immune CLI can hold the guard ~1s past the inner timeout
|
|
244
|
+
* before the `-k` SIGKILL escalation reaps it.
|
|
245
|
+
* - npx (the CLI is a GRANDCHILD: guard → npx → CLI): `-s KILL` — the
|
|
246
|
+
* budget expiry SIGKILLs the whole process group outright. TERM-first
|
|
247
|
+
* would kill only the obedient npx parent, making `timeout` reap it and
|
|
248
|
+
* return before the `-k` escalation ever fires, stranding a
|
|
249
|
+
* SIGTERM-immune CLI grandchild unbounded (reproduced on coreutils
|
|
250
|
+
* 9.x). `-k 1` is retained alongside `-s KILL` as a harmless belt: with
|
|
251
|
+
* `-s KILL` the `-k` escalation signal is also KILL. Two residual gaps
|
|
252
|
+
* on this branch, both bounded by "no worse than pre-fix" (where the
|
|
253
|
+
* grandchild received no signal at all): the group-wide SIGKILL is
|
|
254
|
+
* coreutils semantics — a busybox `timeout` passes the self-test (it
|
|
255
|
+
* has `-k` and propagates exit status) but signals only its direct
|
|
256
|
+
* child, so a busybox guard cannot reach the grandchild; and on the
|
|
257
|
+
* SUPERVISED path (hook alive, inner spawnSync timeout SIGTERMs the
|
|
258
|
+
* guard) coreutils forwards TERM rather than the `-s` signal, npx dies,
|
|
259
|
+
* and the guard exits before any KILL fires — so a SIGTERM-immune CLI
|
|
260
|
+
* grandchild still escapes in those two cases.
|
|
261
|
+
* If the sibling probe predates the resolveUnixGuardTimeout export (version
|
|
262
|
+
* skew), the adapter degrades to the unwrapped invocation instead of
|
|
263
|
+
* throwing. Windows is deliberately NOT wrapped — there is no coreutils
|
|
264
|
+
* timeout to resolve there and the resolver's self-test spawns /bin/sh — so
|
|
265
|
+
* on win32 (the npx.cmd path) and whenever the guard resolves to null (e.g.
|
|
266
|
+
* macOS without Homebrew coreutils — reported once under GITNEXUS_DEBUG)
|
|
267
|
+
* the argv stays byte-identical to the pre-wrap invocation.
|
|
224
268
|
*/
|
|
225
269
|
function runGitNexusCli(cliPath, args, cwd, timeout) {
|
|
226
270
|
const isWin = process.platform === 'win32';
|
|
271
|
+
// Version-skew guard (#2163 follow-up review): an older sibling probe
|
|
272
|
+
// without the resolveUnixGuardTimeout export must degrade to the unwrapped
|
|
273
|
+
// invocation — a TypeError here would be swallowed by the caller's catch
|
|
274
|
+
// and silently kill the augment.
|
|
275
|
+
const guard =
|
|
276
|
+
isWin || typeof resolveUnixGuardTimeout !== 'function' ? null : resolveUnixGuardTimeout();
|
|
277
|
+
if (!isWin && !guard && !unguardedCliWarned && isDebugEnabled()) {
|
|
278
|
+
// Diagnose the "stays unwrapped" Unix paths once per hook process: no
|
|
279
|
+
// usable coreutils timeout/gtimeout (e.g. macOS without Homebrew
|
|
280
|
+
// coreutils), GITNEXUS_HOOK_TIMEOUT_PATH=disabled, or probe skew above.
|
|
281
|
+
unguardedCliWarned = true;
|
|
282
|
+
process.stderr.write(
|
|
283
|
+
'[GitNexus hook] no usable timeout/gtimeout guard; augment CLI child runs unguarded\n',
|
|
284
|
+
);
|
|
285
|
+
}
|
|
227
286
|
if (cliPath) {
|
|
228
|
-
|
|
287
|
+
const [cmd, cmdArgs] = guard
|
|
288
|
+
? [
|
|
289
|
+
guard,
|
|
290
|
+
['-k', '1', String(Math.ceil(timeout / 1000) + 1), process.execPath, cliPath, ...args],
|
|
291
|
+
]
|
|
292
|
+
: [process.execPath, [cliPath, ...args]];
|
|
293
|
+
return spawnSync(cmd, cmdArgs, {
|
|
229
294
|
encoding: 'utf-8',
|
|
230
295
|
timeout,
|
|
231
296
|
cwd,
|
|
@@ -233,8 +298,27 @@ function runGitNexusCli(cliPath, args, cwd, timeout) {
|
|
|
233
298
|
windowsHide: true,
|
|
234
299
|
});
|
|
235
300
|
}
|
|
236
|
-
// On Windows, invoke npx.cmd directly (no shell needed)
|
|
237
|
-
|
|
301
|
+
// On Windows, invoke npx.cmd directly (no shell needed). A non-null guard
|
|
302
|
+
// implies non-Windows, so the wrapped arm can hardcode plain `npx`. The
|
|
303
|
+
// wrapped arm leads with `-s KILL` (NOT TERM-first like the direct branch
|
|
304
|
+
// above): the CLI here is a grandchild behind npx — see the docblock.
|
|
305
|
+
const [cmd, cmdArgs] = guard
|
|
306
|
+
? [
|
|
307
|
+
guard,
|
|
308
|
+
[
|
|
309
|
+
'-s',
|
|
310
|
+
'KILL',
|
|
311
|
+
'-k',
|
|
312
|
+
'1',
|
|
313
|
+
String(Math.ceil((timeout + 5000) / 1000) + 1),
|
|
314
|
+
'npx',
|
|
315
|
+
'-y',
|
|
316
|
+
'gitnexus',
|
|
317
|
+
...args,
|
|
318
|
+
],
|
|
319
|
+
]
|
|
320
|
+
: [isWin ? 'npx.cmd' : 'npx', ['-y', 'gitnexus', ...args]];
|
|
321
|
+
return spawnSync(cmd, cmdArgs, {
|
|
238
322
|
encoding: 'utf-8',
|
|
239
323
|
timeout: timeout + 5000,
|
|
240
324
|
cwd,
|
|
@@ -20,13 +20,18 @@
|
|
|
20
20
|
* it 1s later — orphan lifetime is bounded at ~3s instead of unbounded.
|
|
21
21
|
* - GITNEXUS_HOOK_TIMEOUT_PATH: the sentinel value `disabled` switches the
|
|
22
22
|
* wrapper off deterministically; any other value is adopted only when it
|
|
23
|
-
* exists AND passes a one-shot `-k` self-test — otherwise
|
|
24
|
-
* THROUGH to the built-in candidate list (first self-test
|
|
25
|
-
* no malformed value of any shape can silently disable
|
|
23
|
+
* exists AND passes a one-shot `-k` exit-propagation self-test — otherwise
|
|
24
|
+
* resolution FALLS THROUGH to the built-in candidate list (first self-test
|
|
25
|
+
* pass wins), so no malformed value of any shape can silently disable
|
|
26
|
+
* orphan containment.
|
|
26
27
|
* - The gitnexus server is lazy-open + sticky-hold: an idle MCP server holds
|
|
27
28
|
* ZERO lbug fds until the repo's first MCP query, then keeps the fd open.
|
|
28
29
|
* A probe before that first query is therefore always false — a known,
|
|
29
30
|
* pre-existing race, not a bug in this probe.
|
|
31
|
+
* - resolveUnixGuardTimeout is exported so the hook adapters can wrap the
|
|
32
|
+
* `gitnexus augment` CLI child — the longest-lived hook subprocess (7s
|
|
33
|
+
* local / 12s npx inner budgets) — in the same guard; see runGitNexusCli
|
|
34
|
+
* in the adapters (#2163 follow-up).
|
|
30
35
|
*/
|
|
31
36
|
|
|
32
37
|
const fs = require('fs');
|
|
@@ -70,38 +75,53 @@ let unixGuardTimeoutCache;
|
|
|
70
75
|
|
|
71
76
|
/**
|
|
72
77
|
* Resolve a coreutils `timeout`/`gtimeout` binary to wrap lsof/ps with
|
|
73
|
-
* (#2163).
|
|
78
|
+
* (#2163). Unix-only by contract: the probe's win32 dispatch returns before
|
|
79
|
+
* reaching it, and the exported callers (the adapters' runGitNexusCli,
|
|
80
|
+
* #2163 follow-up) must check the platform first — the self-test below
|
|
81
|
+
* spawns /bin/sh. The memoized result is module-wide, so probe and adapter
|
|
82
|
+
* share one lazy self-test per hook process.
|
|
74
83
|
*
|
|
75
84
|
* GITNEXUS_HOOK_TIMEOUT_PATH semantics: the sentinel `disabled` turns the
|
|
76
85
|
* wrapper off; any other value is only a CANDIDATE — an existing file path
|
|
77
|
-
* is tried first, but it must pass the `-k` self-test to
|
|
78
|
-
* failure (non-existent path, directory, non-executable
|
|
79
|
-
* without `-k` support, …) resolution
|
|
80
|
-
* candidates below, tried in order, first
|
|
81
|
-
* strictly stronger than the sibling
|
|
82
|
-
* GITNEXUS_HOOK_PS_PATH overrides (which only
|
|
83
|
-
* value of ANY shape can silently disable
|
|
86
|
+
* is tried first, but it must pass the `-k` exit-propagation self-test to
|
|
87
|
+
* be adopted. On any failure (non-existent path, directory, non-executable
|
|
88
|
+
* file, wrapper without `-k` support, always-exit-0 stub, …) resolution
|
|
89
|
+
* falls through to the built-in candidates below, tried in order, first
|
|
90
|
+
* self-test pass wins. This is strictly stronger than the sibling
|
|
91
|
+
* GITNEXUS_HOOK_LSOF_PATH / GITNEXUS_HOOK_PS_PATH overrides (which only
|
|
92
|
+
* check existence): no bad env value of ANY shape can silently disable
|
|
93
|
+
* orphan containment.
|
|
84
94
|
*
|
|
85
95
|
* Lazy self-test: candidates are probed only when the lsof/ps fallback is
|
|
86
96
|
* first reached, and the result is memoized. A candidate is adopted only
|
|
87
|
-
* when `timeout -k 1 1 /bin/sh -c
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
97
|
+
* when `timeout -k 1 1 /bin/sh -c 'exit 42'` exits 42 — i.e. it must RUN
|
|
98
|
+
* the wrapped command AND PROPAGATE its exit status. This rejects two
|
|
99
|
+
* failure shapes: wrappers without the coreutils `-k` flag — busybox <1.34,
|
|
100
|
+
* toybox, broken symlinks — which would exit with a usage error without
|
|
101
|
+
* ever running lsof, silently converting the lsof-ETIMEDOUT fail-closed
|
|
102
|
+
* contract into fail-open (#1492 regression); and always-exit-0 stubs
|
|
103
|
+
* (/bin/true shapes), which would otherwise be adopted and "succeed" every
|
|
104
|
+
* wrapped spawn instantly without running it — a constant no-owner probe
|
|
105
|
+
* answer and, worse, a silently dead augment (status 0, empty stderr passes
|
|
106
|
+
* the adapters' success check with no context; #2163 follow-up review).
|
|
107
|
+
* Only when EVERY candidate fails does the probe fall back to the unwrapped
|
|
108
|
+
* status quo (memoized null). busybox ≥1.34 passes the test and is fully
|
|
109
|
+
* usable for everything THIS file spawns (lsof/ps are the guard's direct
|
|
110
|
+
* children) and for the adapters' direct-exec arm. The adapters' npx arm
|
|
111
|
+
* additionally relies on coreutils' process-GROUP signalling for its
|
|
112
|
+
* `-s KILL` grandchild reaping; busybox signals only its direct child, and
|
|
113
|
+
* this self-test deliberately does not probe that capability — see the
|
|
114
|
+
* adapter docblocks for the residual-gap statement.
|
|
95
115
|
*/
|
|
96
116
|
function passesGuardSelfTest(guard) {
|
|
97
117
|
try {
|
|
98
|
-
const selfTest = spawnSync(guard, ['-k', '1', '1', '/bin/sh', '-c', '
|
|
118
|
+
const selfTest = spawnSync(guard, ['-k', '1', '1', '/bin/sh', '-c', 'exit 42'], {
|
|
99
119
|
encoding: 'utf-8',
|
|
100
120
|
timeout: 3000,
|
|
101
121
|
stdio: ['ignore', 'ignore', 'ignore'],
|
|
102
122
|
windowsHide: true,
|
|
103
123
|
});
|
|
104
|
-
return !selfTest.error && selfTest.status ===
|
|
124
|
+
return !selfTest.error && selfTest.status === 42;
|
|
105
125
|
} catch {
|
|
106
126
|
return false;
|
|
107
127
|
}
|
|
@@ -359,4 +379,16 @@ function hasGitNexusDbLockedByGitNexusServer(dbPath, myPid) {
|
|
|
359
379
|
|
|
360
380
|
module.exports = {
|
|
361
381
|
hasGitNexusDbLockedByGitNexusServer,
|
|
382
|
+
// #2163 follow-up: the hook adapters wrap the augment CLI in the same
|
|
383
|
+
// guard. Returns a self-tested wrapper path — the built-in candidates are
|
|
384
|
+
// always absolute; a GITNEXUS_HOOK_TIMEOUT_PATH override is adopted as the
|
|
385
|
+
// exact string that passed the self-test. Same string is also the same
|
|
386
|
+
// RESOLUTION for absolute paths and for slashless names (PATH lookup is
|
|
387
|
+
// cwd-independent); a slash-containing RELATIVE override, however, is
|
|
388
|
+
// existsSync-checked and self-tested against this process's cwd while the
|
|
389
|
+
// adapters spawn the CLI with a `cwd` option (chdir-before-exec), so such
|
|
390
|
+
// a value can pass here yet ENOENT at the augment call site — set the
|
|
391
|
+
// override to an absolute path. Returns null when the wrapper is
|
|
392
|
+
// disabled/unavailable. Never call on win32 (see its JSDoc).
|
|
393
|
+
resolveUnixGuardTimeout,
|
|
362
394
|
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gitnexus-taint-analysis
|
|
3
|
+
description: "Use when working on, reviewing, or extending GitNexus's CFG/taint/PDG subsystem (the `--pdg` layers), or when reasoning about source→sink data-flow findings. Examples: \"How does taint analysis work here?\", \"Why didn't explain find this flow?\", \"Add a new sink/source\", \"Review the interprocedural taint code\"."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# CFG & Taint Analysis with GitNexus
|
|
7
|
+
|
|
8
|
+
Expert knowledge for the opt-in `--pdg` program-analysis subsystem: control-flow
|
|
9
|
+
graphs, reaching definitions, and intra- + inter-procedural taint. Read this
|
|
10
|
+
before touching `gitnexus/src/core/ingestion/cfg/**` or
|
|
11
|
+
`gitnexus/src/core/ingestion/taint/**`, or when explaining a finding.
|
|
12
|
+
|
|
13
|
+
## When to Use
|
|
14
|
+
|
|
15
|
+
- "How does the taint engine work / why is this flow (not) reported?"
|
|
16
|
+
- Adding a source, sink, or sanitizer to the model.
|
|
17
|
+
- Extending or reviewing the CFG / reaching-defs / taint / summary code.
|
|
18
|
+
- Understanding the `explain` MCP tool's findings (intra- vs inter-procedural).
|
|
19
|
+
- Debugging a false positive or false negative in `--pdg` output.
|
|
20
|
+
|
|
21
|
+
## The layered substrate (build order)
|
|
22
|
+
|
|
23
|
+
Taint runs **on** the graph, not beside it. Each layer is opt-in behind `--pdg`
|
|
24
|
+
and a default `analyze` run is **byte-identical** (the golden parity gate is the
|
|
25
|
+
hard floor for every change here).
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
L1 CFG per-function basic blocks + control-flow edges (M1 #2081)
|
|
29
|
+
L2 REACHING_DEF GEN/KILL def→use data dependence (pure solver) (M2 #2082)
|
|
30
|
+
L3 Taint (intra) source→sink over RD facts, minus sanitizers (M3 #2083)
|
|
31
|
+
L4 Taint (inter) per-function summaries composed over CALLS (M4 #2084)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- **Worker-built, main-thread-solved.** The parse worker builds each function's
|
|
35
|
+
CFG + harvests def/use + call-site facts onto `ParsedFile.cfgSideChannel`
|
|
36
|
+
(plain, structured-clone-safe data — never AST nodes). The main thread runs
|
|
37
|
+
the pure solvers. NEVER re-parse on the main thread (re-introduces the #1983
|
|
38
|
+
OOM).
|
|
39
|
+
- **In-phase emit (KTD1).** L1–L4-harvest all run INSIDE the scope-resolution
|
|
40
|
+
pdg window (`scope-resolution/pipeline/run.ts`, gated `input.pdg === true`),
|
|
41
|
+
because the disk-backed ParsedFile store is cleared when that phase ends — a
|
|
42
|
+
standalone post-`mro` phase would read empty data. The cross-function fixpoint
|
|
43
|
+
(L4) is the exception: it runs in its OWN registered phase (`taintSummaries`)
|
|
44
|
+
AFTER scope-resolution, because it needs the COMPLETE call graph, and consumes
|
|
45
|
+
small plain summary data threaded out via `ScopeResolutionOutput`.
|
|
46
|
+
- **Pure-solver contract.** `computeReachingDefs`, `computeTaintFlows`,
|
|
47
|
+
`harvestFunctionSummary`, and `solveInterprocTaint` are pure and deterministic
|
|
48
|
+
(no graph, no I/O, no logger; sorted outputs). Snapshot tests and
|
|
49
|
+
content-derived edge ids depend on it.
|
|
50
|
+
|
|
51
|
+
## Intra-procedural taint (L3)
|
|
52
|
+
|
|
53
|
+
Forward reachability over RD facts from matched **sources** to matched **sinks**,
|
|
54
|
+
killed by **sanitizers**. Key design points worth internalizing:
|
|
55
|
+
|
|
56
|
+
- **Occurrence-tagged sites.** A flat per-arg binding set cannot tell
|
|
57
|
+
`exec(escape(x))` (safe) from `exec(x)` (finding); the harvest records nested
|
|
58
|
+
call structure (`SiteRecord.parent`/via-tags) so sanitizer interposition is
|
|
59
|
+
precise.
|
|
60
|
+
- **Kind-set sanitizer model.** A taint carries a set of *neutralized*
|
|
61
|
+
`SinkKind`s; a sink fires unless its kind is in the set. So `escape(req.body)`
|
|
62
|
+
suppresses `res.send` (xss) but STILL fires `db.query` (sql) — a kind-blind
|
|
63
|
+
kill would be a suppressed live injection (the forbidden FN direction).
|
|
64
|
+
`path.basename(t)` neutralizes path-traversal only, not command-injection.
|
|
65
|
+
- **Statement-level finding identity.** NOT block-pair (block conflation drops
|
|
66
|
+
distinct findings; `exec(req.body, req.query)` is two findings).
|
|
67
|
+
- Persisted as `TAINTED` edges (BasicBlock→BasicBlock); the path rides the
|
|
68
|
+
`reason` column via the shared versioned codec (`taint/path-codec.ts`).
|
|
69
|
+
|
|
70
|
+
## Interprocedural taint (L4) — the functional/summary method
|
|
71
|
+
|
|
72
|
+
The production approach (Sharir-Pnueli 1981; the same shape as Meta's Pysa and
|
|
73
|
+
Mariana Trench, and FB Infer) — NOT full IFDS tabulation. Each function is
|
|
74
|
+
reduced to a compact **summary**, and summaries are composed over the already-
|
|
75
|
+
resolved `CALLS` graph.
|
|
76
|
+
|
|
77
|
+
**Summary shape** (`taint/summary-model.ts`, whole-parameter granularity):
|
|
78
|
+
|
|
79
|
+
| Edge | Meaning | Analogue |
|
|
80
|
+
|------|---------|----------|
|
|
81
|
+
| `param→return` | a param flows to the return value | TITO — **reserved** (the floor already covers its recall; precision pass deferred) |
|
|
82
|
+
| `param→callee-arg` | a param flows into arg *j* of a call (carries the path's neutralized sink kinds) | TITO into callee |
|
|
83
|
+
| `param→sink` | a param reaches a modelled sink | partial/triggered sink |
|
|
84
|
+
| `source→return` | the function generates+returns a source | generative — **composed** via the caller's `callResults` |
|
|
85
|
+
| `source→callee-arg` | a generated source flows into a call | fixpoint SEED |
|
|
86
|
+
| `callResults` | a user-function call's result flows to a sink/return/callee-arg in the caller | composes with callee `source→return` |
|
|
87
|
+
|
|
88
|
+
**The fixpoint** (`taint/interproc-solver.ts`): the unit is `(function,
|
|
89
|
+
parameter, source)`. Seed from `source→callee-arg`, propagate via
|
|
90
|
+
`param→callee-arg`, fire a finding when a tainted param meets `param→sink`.
|
|
91
|
+
|
|
92
|
+
- **Cycle-safe by monotonicity.** The tainted-set is monotone over a finite
|
|
93
|
+
lattice (`fn × param × source`), so the worklist converges — a recursive call
|
|
94
|
+
just re-proposes an already-visited entry. SCC condensation would only refine
|
|
95
|
+
processing order; correctness/termination don't require it.
|
|
96
|
+
- **Source-discriminated state (load-bearing).** Key the state by the SOURCE
|
|
97
|
+
too. Keying only by `(fn, param)` collapses multi-source flows: a sink param
|
|
98
|
+
tainted by source A is marked visited and a later flow from source B is dropped
|
|
99
|
+
before firing — the recurring multi-source bug class. (Bit M3; bit M4 U9.)
|
|
100
|
+
- **Name-based call join.** Match a summary's call-arg edge to a `CALLS` edge by
|
|
101
|
+
CALLEE NAME, not call-site line — line-base parity (CFG 1-based vs reference
|
|
102
|
+
site) is fragile; the callee identity is exact and context-insensitivity
|
|
103
|
+
taints the callee's param identically at every call site.
|
|
104
|
+
- Persisted as `TAINT_PATH` edges (Function→Function), function-level hop chain
|
|
105
|
+
in `reason` via the same codec; confidence < the intra-procedural 1.0.
|
|
106
|
+
|
|
107
|
+
**Context-insensitivity** is the accepted trade-off at this tier: one summary
|
|
108
|
+
per function, return/call-site merging accepted (security-conservative). Expect
|
|
109
|
+
some FP from merging; the bigger FN sources are unmodeled features (below).
|
|
110
|
+
|
|
111
|
+
## Known false-negative classes (documented, deferred)
|
|
112
|
+
|
|
113
|
+
The largest is **closures/callbacks** (`arr.forEach(() => sink(y))`) — taint
|
|
114
|
+
into a callback is dropped without per-library models (true of CodeQL's JS libs
|
|
115
|
+
too). Also deferred: field/property flows (`obj.x = taint; sink(obj.y)`),
|
|
116
|
+
field-sensitive access paths, guard-style sanitizers, implicit/control-dependence
|
|
117
|
+
flows, promise/async-await threading, and **destructured/rest params before a
|
|
118
|
+
tainted simple param** (the summary port index is the binding ordinal, not the
|
|
119
|
+
formal arg position — needs a formal-param index threaded from the worker
|
|
120
|
+
`BindingEntry`). The interprocedural join is also context-insensitive: when one
|
|
121
|
+
caller invokes two distinct **same-named callees**, a flow into one
|
|
122
|
+
over-attributes to both (sound — over-report, never a missed flow). Absence of a
|
|
123
|
+
finding is NOT proof of safety.
|
|
124
|
+
|
|
125
|
+
## GitNexus-specific gotchas
|
|
126
|
+
|
|
127
|
+
- **Function↔CFG join.** `FunctionCfg.functionStartLine` is 1-based; `Function`/
|
|
128
|
+
`Method` node `startLine` is 0-based — join at `startLine - 1`. Function nodes
|
|
129
|
+
have no column, so same-line functions (`{a:()=>x(), b:()=>y()}`) are
|
|
130
|
+
ambiguous → drop (the summary driver counts `unresolved`) rather than
|
|
131
|
+
cross-wire.
|
|
132
|
+
- **No rel-property index (S1).** Kuzu has no secondary index on relationship
|
|
133
|
+
properties, and unanchored `[:TAINTED*]`/`[:TAINT_PATH*]` queries explode.
|
|
134
|
+
TAINT_PATH is therefore MATERIALIZED + anchored at analyze time, never
|
|
135
|
+
traversed live; `explain` reads it source-anchored + LIMIT-guarded.
|
|
136
|
+
- **`explain` is the only discovery surface.** `TAINTED`/`TAINT_PATH` are
|
|
137
|
+
deliberately OUT of `VALID_RELATION_TYPES` (impact's allow-list) and the web
|
|
138
|
+
schema (pinned in `security.test.ts`). `explain` enumerates both layers
|
|
139
|
+
(cross-function findings carry `interprocedural: true`).
|
|
140
|
+
- **One shared codec.** Both the emit path and `explain` import
|
|
141
|
+
`taint/path-codec.ts`. Two hand-rolled copies of a wire format drift — never
|
|
142
|
+
fork it. New metadata extends the format WITHIN the version when writer +
|
|
143
|
+
reader ship together.
|
|
144
|
+
- **Cache versioning.** A worker-harvest shape change bumps the parse-cache pdg
|
|
145
|
+
NAMESPACE (`pdg:N`), NOT `SCHEMA_BUMP` (which cold-invalidates every user).
|
|
146
|
+
Persisted-graph/config changes ride `RepoMeta.pdg`'s key-union mismatch →
|
|
147
|
+
full writeback. Model content rides `taintModelVersion`.
|
|
148
|
+
|
|
149
|
+
## Adding a source / sink / sanitizer
|
|
150
|
+
|
|
151
|
+
Edit the language model in `taint/typescript-model.ts` (registered via the
|
|
152
|
+
explicit `registerBuiltinTaintModels` seam, keyed by `SupportedLanguages`). The
|
|
153
|
+
spec is hashable data (no functions). A sanitizer's `neutralizes` lists the
|
|
154
|
+
EXACT sink kinds it defends — never a blanket kill. Add a fixture + assert the
|
|
155
|
+
finding (or its absence) in `test/unit/taint/` (real-source harness:
|
|
156
|
+
`test/helpers/ts-cfg-harness.ts`); the end-to-end proof is
|
|
157
|
+
`test/integration/cfg/`.
|
|
158
|
+
|
|
159
|
+
## Validation checklist for any `--pdg` change
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
1. tsc clean (schema additions are exhaustiveness-checked; watch the
|
|
163
|
+
api.ts getNodeQuery runtime read-path if a node label is added).
|
|
164
|
+
2. Targeted vitest by directory (test/unit/taint, test/unit/cfg,
|
|
165
|
+
test/integration/cfg) — verify by ISOLATION, not full-suite exit
|
|
166
|
+
(known load-flakes). `node scripts/build.js` before worker/integration runs.
|
|
167
|
+
3. Flag-off golden byte-identical (pipeline-graph-golden.test.ts).
|
|
168
|
+
4. bench/cfg/measure.mjs --check (no fingerprint drift / budget regression).
|
|
169
|
+
5. detect_changes() before commit; impact({direction:'upstream'}) before
|
|
170
|
+
editing shared symbols (KnowledgeGraph, RepoMeta, RelationshipType, codec).
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Prior art (for deeper design questions)
|
|
174
|
+
|
|
175
|
+
Sharir & Pnueli 1981 (functional approach); Reps-Horwitz-Sagiv IFDS (POPL 1995);
|
|
176
|
+
FlowDroid/StubDroid (access-path summaries); Pysa & Mariana Trench (TITO /
|
|
177
|
+
propagations, parallel SCC fixpoint); CodeQL Models-as-Data (the richest port
|
|
178
|
+
notation, incl. callback ports); Infer (content-keyed incremental summaries).
|