gitnexus 1.6.8-rc.20 → 1.6.8-rc.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/cli/ai-context.js +1 -0
  2. package/dist/cli/skill-gen.js +1 -0
  3. package/dist/core/ingestion/cfg/emit.d.ts +16 -1
  4. package/dist/core/ingestion/cfg/emit.js +10 -3
  5. package/dist/core/ingestion/cfg/reaching-defs.d.ts +7 -0
  6. package/dist/core/ingestion/cfg/reaching-defs.js +9 -0
  7. package/dist/core/ingestion/cfg/types.d.ts +91 -0
  8. package/dist/core/ingestion/cfg/visitors/typescript-harvest.d.ts +38 -0
  9. package/dist/core/ingestion/cfg/visitors/typescript-harvest.js +419 -3
  10. package/dist/core/ingestion/pipeline.d.ts +16 -0
  11. package/dist/core/ingestion/scope-resolution/pipeline/phase.js +2 -0
  12. package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +8 -0
  13. package/dist/core/ingestion/scope-resolution/pipeline/run.js +119 -3
  14. package/dist/core/ingestion/taint/emit.d.ts +124 -0
  15. package/dist/core/ingestion/taint/emit.js +204 -0
  16. package/dist/core/ingestion/taint/match.d.ts +153 -0
  17. package/dist/core/ingestion/taint/match.js +278 -0
  18. package/dist/core/ingestion/taint/path-codec.d.ts +134 -0
  19. package/dist/core/ingestion/taint/path-codec.js +190 -0
  20. package/dist/core/ingestion/taint/propagate.d.ts +216 -0
  21. package/dist/core/ingestion/taint/propagate.js +664 -0
  22. package/dist/core/ingestion/taint/site-safety.d.ts +29 -0
  23. package/dist/core/ingestion/taint/site-safety.js +98 -0
  24. package/dist/core/ingestion/taint/source-sink-config.d.ts +94 -23
  25. package/dist/core/ingestion/taint/source-sink-config.js +11 -11
  26. package/dist/core/ingestion/taint/source-sink-registry.d.ts +6 -4
  27. package/dist/core/ingestion/taint/source-sink-registry.js +6 -4
  28. package/dist/core/ingestion/taint/typescript-model.d.ts +38 -0
  29. package/dist/core/ingestion/taint/typescript-model.js +102 -0
  30. package/dist/core/run-analyze.d.ts +8 -1
  31. package/dist/core/run-analyze.js +14 -0
  32. package/dist/mcp/local/local-backend.d.ts +29 -0
  33. package/dist/mcp/local/local-backend.js +241 -1
  34. package/dist/mcp/resources.js +1 -0
  35. package/dist/mcp/tools.d.ts +9 -0
  36. package/dist/mcp/tools.js +53 -0
  37. package/dist/storage/parse-cache.js +10 -1
  38. package/dist/storage/repo-manager.d.ts +18 -0
  39. package/package.json +1 -1
  40. package/skills/gitnexus-guide.md +11 -0
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Taint-site safety validation (#2083 M3 U1, plan KTD2).
3
+ *
4
+ * Mirrors `hasEmitSafeFacts` (cfg/emit.ts): an untrusted `cfgSideChannel`
5
+ * element — possibly from a corrupted durable parsedfile store — must never
6
+ * crash the taint pass or fabricate matches from out-of-range indices. The
7
+ * degradation contract is per-FUNCTION and one-directional: a CFG whose sites
8
+ * fail this check is SKIPPED FOR TAINT ONLY — the BasicBlock/CFG layer and
9
+ * the REACHING_DEF projection (guarded by their own checks) are unaffected.
10
+ *
11
+ * Checked: exactly the indices the taint matcher dereferences — binding
12
+ * indices (`receiver`/`object`/`resultDefs`/arg occurrences) against the
13
+ * function's binding table, and intra-statement site references (`parent`
14
+ * site / via-tags) against the OWNING statement's `sites` array. Site
15
+ * references are statement-local by construction (each statement's
16
+ * FactAccumulator starts at index 0); a cross-statement reference is
17
+ * corruption, not a feature.
18
+ *
19
+ * Lives in `taint/` (not cfg/emit.ts): U4's taint emit path is the only
20
+ * consumer, and the guard must evolve with the matcher that dereferences
21
+ * these fields.
22
+ */
23
+ import type { FunctionCfg } from '../cfg/types.js';
24
+ /**
25
+ * Whether a structurally-valid CFG's M3 `sites` annotations are safe to feed
26
+ * to the taint matcher/propagator. `true` when no statement carries sites
27
+ * (pre-M3 channel, or no calls) — absence is the well-formed empty case.
28
+ */
29
+ export declare const hasTaintSafeSites: (cfg: FunctionCfg) => boolean;
@@ -0,0 +1,98 @@
1
+ const SITE_KINDS = new Set(['call', 'new', 'member-read']);
2
+ /**
3
+ * Whether a structurally-valid CFG's M3 `sites` annotations are safe to feed
4
+ * to the taint matcher/propagator. `true` when no statement carries sites
5
+ * (pre-M3 channel, or no calls) — absence is the well-formed empty case.
6
+ */
7
+ export const hasTaintSafeSites = (cfg) => {
8
+ // Sites carry binding indices — a channel with sites but no binding table
9
+ // has nothing to range-check them against: reject (checked per statement).
10
+ const bindingCount = Array.isArray(cfg.bindings) ? cfg.bindings.length : -1;
11
+ for (const block of cfg.blocks) {
12
+ const stmts = block.statements;
13
+ if (stmts === undefined)
14
+ continue;
15
+ if (!Array.isArray(stmts))
16
+ return false;
17
+ for (const s of stmts) {
18
+ if (s?.sites === undefined)
19
+ continue;
20
+ if (bindingCount < 0)
21
+ return false;
22
+ if (!isSafeSiteList(s.sites, bindingCount))
23
+ return false;
24
+ }
25
+ }
26
+ return true;
27
+ };
28
+ const isSafeSiteList = (sites, bindingCount) => {
29
+ if (!Array.isArray(sites))
30
+ return false;
31
+ const siteCount = sites.length;
32
+ const bindingInRange = (i) => Number.isInteger(i) && i >= 0 && i < bindingCount;
33
+ const siteInRange = (i) => Number.isInteger(i) && i >= 0 && i < siteCount;
34
+ for (const site of sites) {
35
+ if (site === null || typeof site !== 'object')
36
+ return false;
37
+ if (typeof site.kind !== 'string' || !SITE_KINDS.has(site.kind))
38
+ return false;
39
+ if (site.callee !== undefined && typeof site.callee !== 'string')
40
+ return false;
41
+ if (site.receiver !== undefined && !bindingInRange(site.receiver))
42
+ return false;
43
+ if (site.requireArg !== undefined && typeof site.requireArg !== 'string')
44
+ return false;
45
+ if (site.template !== undefined && typeof site.template !== 'boolean')
46
+ return false;
47
+ if (site.spread !== undefined &&
48
+ (!Number.isInteger(site.spread) || site.spread < 0)) {
49
+ return false;
50
+ }
51
+ if (site.parent !== undefined) {
52
+ const p = site.parent;
53
+ if (!Array.isArray(p) || p.length !== 2)
54
+ return false;
55
+ if (!siteInRange(p[0]))
56
+ return false;
57
+ if (!Number.isInteger(p[1]) || p[1] < 0)
58
+ return false;
59
+ }
60
+ if (site.resultDefs !== undefined) {
61
+ if (!Array.isArray(site.resultDefs) || !site.resultDefs.every(bindingInRange))
62
+ return false;
63
+ }
64
+ if (site.args !== undefined) {
65
+ if (!Array.isArray(site.args))
66
+ return false;
67
+ for (const position of site.args) {
68
+ if (!Array.isArray(position))
69
+ return false;
70
+ for (const entry of position) {
71
+ if (typeof entry === 'number') {
72
+ if (!bindingInRange(entry))
73
+ return false;
74
+ }
75
+ else if (Array.isArray(entry) && entry.length === 2) {
76
+ if (!bindingInRange(entry[0]) || !siteInRange(entry[1]))
77
+ return false;
78
+ }
79
+ else {
80
+ return false;
81
+ }
82
+ }
83
+ }
84
+ }
85
+ if (site.kind === 'member-read') {
86
+ // The matcher dereferences both unconditionally on member reads.
87
+ if (!bindingInRange(site.object) || typeof site.property !== 'string')
88
+ return false;
89
+ }
90
+ else {
91
+ if (site.object !== undefined && !bindingInRange(site.object))
92
+ return false;
93
+ if (site.property !== undefined && typeof site.property !== 'string')
94
+ return false;
95
+ }
96
+ }
97
+ return true;
98
+ };
@@ -1,36 +1,107 @@
1
1
  /**
2
- * Source/sink/sanitizer config model (issue #2080, taint/PDG substrate M0).
2
+ * Source/sink/sanitizer config model (issue #2080 M0 seam, extended by #2083
3
+ * M3 U2).
3
4
  *
4
- * The per-language taint configuration *shape*. M0 ships only the type and an
5
- * (empty) registry seam no analysis consumes it yet. M3 (#2083, intra-proc
6
- * taint) populates per-language specs and reads them when emitting TAINTED /
7
- * SANITIZES edges.
5
+ * The per-language taint configuration *shape*. M0 shipped only the bare
6
+ * `{name, args?}` callable matcher and an empty registry seam; M3 U2 extends
7
+ * it with the `kind` taxonomy and the resolution-mechanism fields the
8
+ * import-aware matcher (`taint/match.ts`) needs, and fills the registry with
9
+ * the built-in TS/JS model (`taint/typescript-model.ts`).
8
10
  *
9
- * Kept deliberately minimal: enough for M3 to express "callable X is a
10
- * source / sink / sanitizer, optionally for argument position N" without M0
11
- * committing to matcher semantics it cannot yet validate. The shape is
12
- * expected to grow (e.g. sanitizer escape conditions, return-position taint)
13
- * when M3 makes contact with real flows; that is a forward-declared-interface
14
- * design choice, not a finished contract.
11
+ * Design rule: entries describe WHAT a callable is (category + how its name
12
+ * resolves), never HOW matching works matching semantics (import joins,
13
+ * shadow checks, spread/template position rules) live in the matcher so the
14
+ * spec stays declarative data that can hash into `taintModelVersion`.
15
15
  */
16
+ /** Categories of taint sources. M3 ships remote HTTP input only. */
17
+ export type SourceKind = 'remote-input';
16
18
  /**
17
- * Identifies a callable that participates in taint flow. `name` is matched
18
- * against a resolved callable (simple or qualified name — exact matching
19
- * semantics are M3's call). `args` optionally narrows to specific 0-based
20
- * argument positions that carry taint (for a source/sink) or clear it (for a
21
- * sanitizer); omit to mean "unspecified / all".
19
+ * Vulnerability categories for sinks. Sanitizers reference the SAME taxonomy
20
+ * via {@link TaintSanitizerEntry.neutralizes}: a sanitizer kill applies only
21
+ * when it neutralizes the matched sink's kind (`path.basename` strips
22
+ * directories, not shell metacharacters a kind-blind kill is a suppressed
23
+ * live command injection, the forbidden false-negative direction).
22
24
  */
23
- export interface TaintCallableMatcher {
25
+ export type SinkKind = 'command-injection' | 'code-injection' | 'path-traversal' | 'sql-injection' | 'xss';
26
+ /**
27
+ * Identifies a callable that participates in taint flow. `name` is the
28
+ * callable's own (unqualified) name — qualification comes from the
29
+ * resolution-mechanism fields on the extending entry types, not from dotted
30
+ * `name` strings. `args` optionally narrows to specific 0-based argument
31
+ * positions that carry taint into a sink (or are cleared by a sanitizer);
32
+ * omit to mean "all positions".
33
+ */
34
+ export interface TaintCallableMatcher<K extends string = string> {
35
+ readonly name: string;
36
+ readonly args?: readonly number[];
37
+ /** Category label — drives finding classification and sanitizer kind-compat. */
38
+ readonly kind: K;
39
+ }
40
+ /**
41
+ * A sink callable. Exactly one resolution mechanism should be set per entry:
42
+ *
43
+ * - `module` — the callable lives in a package/builtin module; the matcher
44
+ * resolves call sites against it import-aware (ESM `parsedImports` aliases,
45
+ * namespace handles, and the CommonJS `require('<literal>')` join). `name`
46
+ * is the exported member (`'exec'` of `'child_process'`); the pseudo-name
47
+ * `'default'` denotes invoking the module's default export / the module
48
+ * handle itself.
49
+ * - `global` — a true ECMAScript global (`eval`, `Function`); matched by bare
50
+ * name only when the name is not shadowed by an in-function declaration and
51
+ * not bound by an import. `newOnly` further restricts to `new` expressions
52
+ * (`new Function(body)`).
53
+ * - `anyReceiver` — a method matched on ANY receiver chain by its final
54
+ * segment (`.query(sql)` / `.execute(sql)` on whatever the DB handle is
55
+ * named) — deliberately name-conventional, like Semgrep's default rules.
56
+ * - `receivers` — a method matched only on the listed conventional receiver
57
+ * names (`res.send` / `res.write`); exactly `<receiver>.<name>`, name-based.
58
+ */
59
+ export interface TaintSinkEntry extends TaintCallableMatcher<SinkKind> {
60
+ readonly module?: string;
61
+ readonly global?: boolean;
62
+ /** Only meaningful with `global`: match `new <name>(…)` sites only. */
63
+ readonly newOnly?: boolean;
64
+ readonly anyReceiver?: boolean;
65
+ readonly receivers?: readonly string[];
66
+ }
67
+ /**
68
+ * A sanitizer callable. Carries the sink kinds it `neutralizes` instead of a
69
+ * `kind` of its own. STRICTER resolution than sinks by design: only the
70
+ * `module` (import-aware) and `global` mechanisms exist — never a bare-name
71
+ * convention — because a sanitizer mis-match is a false KILL (a user's own
72
+ * `escape` helper must not suppress findings), while a sink mis-match is
73
+ * merely noise. `args` narrows which argument positions are cleared (omit =
74
+ * all).
75
+ */
76
+ export interface TaintSanitizerEntry {
24
77
  readonly name: string;
25
78
  readonly args?: readonly number[];
79
+ readonly neutralizes: readonly SinkKind[];
80
+ readonly module?: string;
81
+ readonly global?: boolean;
82
+ }
83
+ /**
84
+ * A member-read taint source: reading `<object>.<property>` where the object
85
+ * is one of the conventional receiver `objects` names (`req`/`request`) and
86
+ * the property is one of `properties` (`body`, `query`, …). Matching is
87
+ * name-based on the harvested `member-read` site (Semgrep-convention, not
88
+ * type-aware — the accepted M3 FP/FN trade recorded in the plan's risk
89
+ * table). One entry fans out over the objects × properties product.
90
+ */
91
+ export interface TaintMemberSourceEntry {
92
+ readonly kind: SourceKind;
93
+ readonly objects: readonly string[];
94
+ readonly properties: readonly string[];
26
95
  }
27
96
  /**
28
- * The taint configuration for a single language: which callables introduce
29
- * taint (sources), which are dangerous to reach with tainted input (sinks),
30
- * and which clear taint (sanitizers).
97
+ * The taint configuration for a single language: which member reads introduce
98
+ * taint (sources), which callables are dangerous to reach with tainted input
99
+ * (sinks), and which callables clear it (sanitizers). M3 sources are
100
+ * member-read entries only; call-result sources are a forward extension
101
+ * (add a union variant), not a missing case.
31
102
  */
32
103
  export interface SourceSinkSanitizerSpec {
33
- readonly sources: readonly TaintCallableMatcher[];
34
- readonly sinks: readonly TaintCallableMatcher[];
35
- readonly sanitizers: readonly TaintCallableMatcher[];
104
+ readonly sources: readonly TaintMemberSourceEntry[];
105
+ readonly sinks: readonly TaintSinkEntry[];
106
+ readonly sanitizers: readonly TaintSanitizerEntry[];
36
107
  }
@@ -1,16 +1,16 @@
1
1
  /**
2
- * Source/sink/sanitizer config model (issue #2080, taint/PDG substrate M0).
2
+ * Source/sink/sanitizer config model (issue #2080 M0 seam, extended by #2083
3
+ * M3 U2).
3
4
  *
4
- * The per-language taint configuration *shape*. M0 ships only the type and an
5
- * (empty) registry seam no analysis consumes it yet. M3 (#2083, intra-proc
6
- * taint) populates per-language specs and reads them when emitting TAINTED /
7
- * SANITIZES edges.
5
+ * The per-language taint configuration *shape*. M0 shipped only the bare
6
+ * `{name, args?}` callable matcher and an empty registry seam; M3 U2 extends
7
+ * it with the `kind` taxonomy and the resolution-mechanism fields the
8
+ * import-aware matcher (`taint/match.ts`) needs, and fills the registry with
9
+ * the built-in TS/JS model (`taint/typescript-model.ts`).
8
10
  *
9
- * Kept deliberately minimal: enough for M3 to express "callable X is a
10
- * source / sink / sanitizer, optionally for argument position N" without M0
11
- * committing to matcher semantics it cannot yet validate. The shape is
12
- * expected to grow (e.g. sanitizer escape conditions, return-position taint)
13
- * when M3 makes contact with real flows; that is a forward-declared-interface
14
- * design choice, not a finished contract.
11
+ * Design rule: entries describe WHAT a callable is (category + how its name
12
+ * resolves), never HOW matching works matching semantics (import joins,
13
+ * shadow checks, spread/template position rules) live in the matcher so the
14
+ * spec stays declarative data that can hash into `taintModelVersion`.
15
15
  */
16
16
  export {};
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * Per-language source/sink/sanitizer registry seam (issue #2080).
3
3
  *
4
- * A keyed registry of {@link SourceSinkSanitizerSpec} by language id. M0 stands
5
- * up the empty seam no language is registered and nothing in the pipeline
6
- * reads it. M3 (#2083) registers per-language specs and queries this registry
7
- * when emitting taint edges.
4
+ * A keyed registry of {@link SourceSinkSanitizerSpec} by language id. M0 stood
5
+ * up the empty seam; M3 U2 (#2083) fills it with the built-in TS/JS model via
6
+ * the EXPLICIT `registerBuiltinTaintModels()` seam in `typescript-model.ts`
7
+ * deliberately not an import side-effect. The U4 taint emit path must call it
8
+ * once before the pdg window consumes the registry (idempotent; the registry
9
+ * itself stays empty until then, preserving default-run parity).
8
10
  *
9
11
  * The store is module-level (matching the codebase's other per-language
10
12
  * registries). {@link clearSourceSinkRegistry} resets it for test isolation.
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * Per-language source/sink/sanitizer registry seam (issue #2080).
3
3
  *
4
- * A keyed registry of {@link SourceSinkSanitizerSpec} by language id. M0 stands
5
- * up the empty seam no language is registered and nothing in the pipeline
6
- * reads it. M3 (#2083) registers per-language specs and queries this registry
7
- * when emitting taint edges.
4
+ * A keyed registry of {@link SourceSinkSanitizerSpec} by language id. M0 stood
5
+ * up the empty seam; M3 U2 (#2083) fills it with the built-in TS/JS model via
6
+ * the EXPLICIT `registerBuiltinTaintModels()` seam in `typescript-model.ts`
7
+ * deliberately not an import side-effect. The U4 taint emit path must call it
8
+ * once before the pdg window consumes the registry (idempotent; the registry
9
+ * itself stays empty until then, preserving default-run parity).
8
10
  *
9
11
  * The store is module-level (matching the codebase's other per-language
10
12
  * registries). {@link clearSourceSinkRegistry} resets it for test isolation.
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Built-in TS/JS taint model (#2083 M3 U2, plan KTD7).
3
+ *
4
+ * The canonical Express/Node source/sink/sanitizer set, registered for the
5
+ * `typescript` and `javascript` language ids via the EXPLICIT
6
+ * {@link registerBuiltinTaintModels} seam — deliberately not an import
7
+ * side-effect, so the U4 emit path controls WHEN registration happens (call
8
+ * it once before the pdg window runs; it is idempotent — the registry is
9
+ * last-write-wins on the same language id).
10
+ *
11
+ * `taintModelVersion` is a deterministic digest of the FULL model content
12
+ * (entries, kinds, args, modules). It joins the RepoMeta `pdg` stamp in U5 so
13
+ * that ANY model change — adding an entry, relabeling a kind — trips full
14
+ * writeback on an existing `--pdg` index (R7): persisted findings must never
15
+ * outlive the model that produced them.
16
+ */
17
+ import type { SourceSinkSanitizerSpec } from './source-sink-config.js';
18
+ /**
19
+ * The built-in TS/JS model. Module provenance uses bare specifier names —
20
+ * the matcher normalizes the `node:` scheme prefix, so `import { exec } from
21
+ * 'node:child_process'` resolves identically.
22
+ */
23
+ export declare const TS_JS_TAINT_MODEL: SourceSinkSanitizerSpec;
24
+ /**
25
+ * Deterministic digest of a spec's full content. Key order is canonicalized
26
+ * (recursively sorted) so the version reflects CONTENT, not literal layout;
27
+ * array order is semantic (entry identity) and intentionally preserved.
28
+ */
29
+ export declare function computeTaintModelVersion(spec: SourceSinkSanitizerSpec): string;
30
+ /** Version stamp of the built-in TS/JS model (joins the RepoMeta pdg key in U5). */
31
+ export declare const taintModelVersion: string;
32
+ /**
33
+ * Register the built-in model for TypeScript and JavaScript. Explicit init
34
+ * seam for the U4 emit path (call before the pdg window consumes the
35
+ * registry); idempotent. Vue and other TS-adjacent language ids are
36
+ * deliberately NOT registered — the M3 scope is TS/JS only.
37
+ */
38
+ export declare function registerBuiltinTaintModels(): void;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Built-in TS/JS taint model (#2083 M3 U2, plan KTD7).
3
+ *
4
+ * The canonical Express/Node source/sink/sanitizer set, registered for the
5
+ * `typescript` and `javascript` language ids via the EXPLICIT
6
+ * {@link registerBuiltinTaintModels} seam — deliberately not an import
7
+ * side-effect, so the U4 emit path controls WHEN registration happens (call
8
+ * it once before the pdg window runs; it is idempotent — the registry is
9
+ * last-write-wins on the same language id).
10
+ *
11
+ * `taintModelVersion` is a deterministic digest of the FULL model content
12
+ * (entries, kinds, args, modules). It joins the RepoMeta `pdg` stamp in U5 so
13
+ * that ANY model change — adding an entry, relabeling a kind — trips full
14
+ * writeback on an existing `--pdg` index (R7): persisted findings must never
15
+ * outlive the model that produced them.
16
+ */
17
+ import { createHash } from 'node:crypto';
18
+ import { SupportedLanguages } from '../../../_shared/index.js';
19
+ import { registerSourceSinkConfig } from './source-sink-registry.js';
20
+ /**
21
+ * The built-in TS/JS model. Module provenance uses bare specifier names —
22
+ * the matcher normalizes the `node:` scheme prefix, so `import { exec } from
23
+ * 'node:child_process'` resolves identically.
24
+ */
25
+ export const TS_JS_TAINT_MODEL = {
26
+ sources: [
27
+ // Express-convention request member reads, matched name-based on the
28
+ // receiver (`req`/`request`) — the plan's accepted Semgrep-style trade.
29
+ {
30
+ kind: 'remote-input',
31
+ objects: ['req', 'request'],
32
+ properties: ['body', 'query', 'params', 'headers', 'cookies'],
33
+ },
34
+ ],
35
+ sinks: [
36
+ // Command execution — the command string is argument 0.
37
+ { name: 'exec', kind: 'command-injection', args: [0], module: 'child_process' },
38
+ { name: 'execSync', kind: 'command-injection', args: [0], module: 'child_process' },
39
+ { name: 'spawn', kind: 'command-injection', args: [0], module: 'child_process' },
40
+ // Code evaluation. `eval` takes code at 0; `new Function(...)` treats
41
+ // EVERY argument as source text (params + body), so `args` is omitted
42
+ // (= all positions) rather than pinned to 0.
43
+ { name: 'eval', kind: 'code-injection', args: [0], global: true },
44
+ { name: 'Function', kind: 'code-injection', global: true, newOnly: true },
45
+ // Filesystem path consumption — path argument 0.
46
+ { name: 'readFile', kind: 'path-traversal', args: [0], module: 'fs' },
47
+ { name: 'readFileSync', kind: 'path-traversal', args: [0], module: 'fs' },
48
+ { name: 'writeFile', kind: 'path-traversal', args: [0], module: 'fs' },
49
+ { name: 'writeFileSync', kind: 'path-traversal', args: [0], module: 'fs' },
50
+ // SQL — `.query(sql)` / `.execute(sql)` member calls on ANY receiver
51
+ // (mysql2/pg/knex handles go by many names; receiver-conventional).
52
+ { name: 'query', kind: 'sql-injection', args: [0], anyReceiver: true },
53
+ { name: 'execute', kind: 'sql-injection', args: [0], anyReceiver: true },
54
+ // Reflected XSS — Express response writes, conventional receiver `res`.
55
+ { name: 'send', kind: 'xss', args: [0], receivers: ['res'] },
56
+ { name: 'write', kind: 'xss', args: [0], receivers: ['res'] },
57
+ ],
58
+ sanitizers: [
59
+ // URL-encoding: neutralizes markup injection AND path separators
60
+ // (`%2F` is not a separator inside a path component).
61
+ { name: 'encodeURIComponent', neutralizes: ['xss', 'path-traversal'], global: true },
62
+ // `escape-html` exports its function as the module default — the
63
+ // `'default'` pseudo-name matches the default-imported / require'd
64
+ // module handle being invoked directly.
65
+ { name: 'default', neutralizes: ['xss'], module: 'escape-html' },
66
+ { name: 'encode', neutralizes: ['xss'], module: 'he' },
67
+ { name: 'basename', neutralizes: ['path-traversal'], module: 'path' },
68
+ { name: 'escape', neutralizes: ['xss'], module: 'validator' },
69
+ ],
70
+ };
71
+ /**
72
+ * Deterministic digest of a spec's full content. Key order is canonicalized
73
+ * (recursively sorted) so the version reflects CONTENT, not literal layout;
74
+ * array order is semantic (entry identity) and intentionally preserved.
75
+ */
76
+ export function computeTaintModelVersion(spec) {
77
+ return createHash('sha256').update(canonicalJson(spec)).digest('hex').slice(0, 12);
78
+ }
79
+ function canonicalJson(value) {
80
+ if (Array.isArray(value))
81
+ return `[${value.map(canonicalJson).join(',')}]`;
82
+ if (value !== null && typeof value === 'object') {
83
+ const entries = Object.entries(value)
84
+ .filter(([, v]) => v !== undefined)
85
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
86
+ .map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`);
87
+ return `{${entries.join(',')}}`;
88
+ }
89
+ return JSON.stringify(value);
90
+ }
91
+ /** Version stamp of the built-in TS/JS model (joins the RepoMeta pdg key in U5). */
92
+ export const taintModelVersion = computeTaintModelVersion(TS_JS_TAINT_MODEL);
93
+ /**
94
+ * Register the built-in model for TypeScript and JavaScript. Explicit init
95
+ * seam for the U4 emit path (call before the pdg window consumes the
96
+ * registry); idempotent. Vue and other TS-adjacent language ids are
97
+ * deliberately NOT registered — the M3 scope is TS/JS only.
98
+ */
99
+ export function registerBuiltinTaintModels() {
100
+ registerSourceSinkConfig(SupportedLanguages.TypeScript, TS_JS_TAINT_MODEL);
101
+ registerSourceSinkConfig(SupportedLanguages.JavaScript, TS_JS_TAINT_MODEL);
102
+ }
@@ -65,6 +65,13 @@ export interface AnalyzeOptions {
65
65
  /** Per-function REACHING_DEF edge cap (#2082 M2). Forwarded to
66
66
  * `PipelineOptions.pdgMaxReachingDefEdgesPerFunction`. */
67
67
  pdgMaxReachingDefEdgesPerFunction?: number;
68
+ /** Per-function taint findings cap (#2083 M3). Forwarded to
69
+ * `PipelineOptions.pdgMaxTaintFindingsPerFunction`. No CLI flag or rc key
70
+ * (KTD8) — programmatic / server path only, like the other pdg caps. */
71
+ pdgMaxTaintFindingsPerFunction?: number;
72
+ /** Per-finding taint hop cap (#2083 M3, KTD6). Forwarded to
73
+ * `PipelineOptions.pdgMaxTaintHops`. No CLI flag or rc key (KTD8). */
74
+ pdgMaxTaintHops?: number;
68
75
  /**
69
76
  * Default branch threaded into generated AGENTS.md / CLAUDE.md so the
70
77
  * regression-compare example uses the configured branch instead of a
@@ -185,7 +192,7 @@ export declare const collectBranchCacheKeys: (storagePath: string, excludeDir?:
185
192
  * defaults so an explicit-default run compares equal to a default run
186
193
  * (`0` = unlimited is preserved as `0`). Pure + exported for testing.
187
194
  */
188
- type PdgOptions = Pick<AnalyzeOptions, 'pdg' | 'pdgMaxFunctionLines' | 'pdgMaxEdgesPerFunction' | 'pdgMaxReachingDefEdgesPerFunction'>;
195
+ type PdgOptions = Pick<AnalyzeOptions, 'pdg' | 'pdgMaxFunctionLines' | 'pdgMaxEdgesPerFunction' | 'pdgMaxReachingDefEdgesPerFunction' | 'pdgMaxTaintFindingsPerFunction' | 'pdgMaxTaintHops'>;
189
196
  export declare const resolvePdgConfig: (options: PdgOptions) => RepoMeta["pdg"];
190
197
  /**
191
198
  * Whether the requested `--pdg` configuration differs from the one the
@@ -20,6 +20,8 @@ import { startWalCheckpointDriver, } from './lbug/wal-checkpoint-driver.js';
20
20
  import { getStoragePaths, resolveBranchPlacement, saveMeta, loadMeta, ensureGitNexusIgnored, registerRepo, cleanupOldKuzuFiles, INCREMENTAL_SCHEMA_VERSION, } from '../storage/repo-manager.js';
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
+ import { DEFAULT_PDG_MAX_TAINT_FINDINGS_PER_FUNCTION, DEFAULT_PDG_MAX_TAINT_HOPS, } from './ingestion/taint/propagate.js';
24
+ import { taintModelVersion } from './ingestion/taint/typescript-model.js';
23
25
  import { computeFileHashes, diffFileHashes } from '../storage/file-hash.js';
24
26
  import { extractChangedSubgraph, computeEffectiveWriteSet, } from './incremental/subgraph-extract.js';
25
27
  import { shadowCandidatesFor } from './incremental/shadow-candidates.js';
@@ -136,6 +138,16 @@ export const resolvePdgConfig = (options) => options.pdg === true
136
138
  maxEdgesPerFunction: options.pdgMaxEdgesPerFunction ?? DEFAULT_MAX_CFG_EDGES_PER_FUNCTION,
137
139
  maxReachingDefEdgesPerFunction: options.pdgMaxReachingDefEdgesPerFunction ??
138
140
  DEFAULT_PDG_MAX_REACHING_DEF_EDGES_PER_FUNCTION,
141
+ // #2083 M3: taint caps + model identity. The key-union comparator in
142
+ // pdgModeMismatch picks these up structurally — an M2-era stamp lacks
143
+ // all three, so the first M3 run over an M2 `--pdg` index trips a full
144
+ // writeback that populates TAINTED/SANITIZES rows without `--force`.
145
+ maxTaintFindingsPerFunction: options.pdgMaxTaintFindingsPerFunction ?? DEFAULT_PDG_MAX_TAINT_FINDINGS_PER_FUNCTION,
146
+ maxTaintHops: options.pdgMaxTaintHops ?? DEFAULT_PDG_MAX_TAINT_HOPS,
147
+ // Built-in model digest (KTD7/R7): persisted findings must never
148
+ // outlive the model that produced them — ANY model-content change
149
+ // ships as a new digest and repopulates the taint edges.
150
+ taintModelVersion,
139
151
  }
140
152
  : undefined;
141
153
  /**
@@ -487,6 +499,8 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
487
499
  pdgMaxFunctionLines: options.pdgMaxFunctionLines,
488
500
  pdgMaxEdgesPerFunction: options.pdgMaxEdgesPerFunction,
489
501
  pdgMaxReachingDefEdgesPerFunction: options.pdgMaxReachingDefEdgesPerFunction,
502
+ pdgMaxTaintFindingsPerFunction: options.pdgMaxTaintFindingsPerFunction,
503
+ pdgMaxTaintHops: options.pdgMaxTaintHops,
490
504
  fetchWrappers: options.fetchWrappers,
491
505
  });
492
506
  // ── Phase 2: LadybugDB (60–85%) ──────────────────────────────────
@@ -424,6 +424,35 @@ export declare class LocalBackend {
424
424
  */
425
425
  private context;
426
426
  private _contextImpl;
427
+ /**
428
+ * Explain tool (#2083 M3 U6) — persisted taint-finding explanation.
429
+ * WAL-aware wrapper mirroring `context`.
430
+ */
431
+ private explain;
432
+ /**
433
+ * Taint findings are persisted as `TAINTED` rows in CodeRelation whose
434
+ * endpoints are BOTH BasicBlock nodes — the label anchor restricts every
435
+ * query here to the BasicBlock→BasicBlock partition of the rel table
436
+ * (which holds only the sparse, per-function-capped pdg layers), never a
437
+ * global symbol-space scan (the S1 verdict; LadybugDB has no rel-property
438
+ * index, so the label anchor IS the bound).
439
+ *
440
+ * Anchoring granularity:
441
+ * - file target → BasicBlock id prefix (`BasicBlock:<filePath>:` — the
442
+ * shared `basicBlockId` template) with an exact-or-suffix path match so
443
+ * `vuln.ts` finds `src/vuln.ts`.
444
+ * - symbol target → resolved via `resolveSymbolCandidates` (the context()
445
+ * path: ambiguous ⇒ ranked candidates, unknown ⇒ not-found), then the
446
+ * file id-prefix PLUS source-block startLine within the symbol's
447
+ * [startLine, endLine] span. Findings are intra-procedural, so filtering
448
+ * the SOURCE endpoint is sufficient — both endpoints share the function.
449
+ * Symbols without a line span degrade to the file-level filter.
450
+ *
451
+ * The per-finding `sinkKind` and hop path decode from the persisted
452
+ * `reason` via the SHARED `taint/path-codec.ts` (the U4 write path encodes
453
+ * with the same module — `;<kind>` header + ordered `variable:line` hops).
454
+ */
455
+ private _explainImpl;
427
456
  /**
428
457
  * Legacy explore — kept for backwards compatibility with resources.ts.
429
458
  * Routes cluster/process types to direct graph queries.