graph-indexer 2.1.1 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +312 -1
- package/README.md +236 -312
- package/SECURITY.md +53 -58
- package/bin/graph-indexer.mjs +251 -0
- package/integrations/claude-code/.claude-plugin/plugin.json +9 -0
- package/integrations/claude-code/.mcp.json +8 -0
- package/integrations/claude-code/agents/code-structure.md +22 -0
- package/integrations/claude-code/hooks/gi-hook.mjs +103 -0
- package/integrations/claude-code/hooks/hooks.json +16 -0
- package/integrations/opencode/graph-indexer.js +129 -0
- package/package.json +32 -109
- package/src/cli/helper.mjs +67 -0
- package/src/cli/hook.mjs +366 -0
- package/src/cli/init.mjs +272 -0
- package/src/cli/resident.mjs +185 -0
- package/src/index/discover.mjs +128 -0
- package/src/index/fingerprint.mjs +30 -0
- package/src/index/graph.mjs +61 -0
- package/src/index/indexer.mjs +476 -0
- package/src/index/modules.mjs +332 -0
- package/src/index/resolver.mjs +961 -0
- package/src/mcp/render.mjs +60 -0
- package/src/mcp/server.mjs +147 -0
- package/src/mcp/tools.mjs +974 -0
- package/src/parse/arity.mjs +153 -0
- package/src/parse/extract.mjs +1038 -0
- package/src/parse/lang/csharp.mjs +106 -0
- package/src/parse/lang/ecma.mjs +438 -0
- package/src/parse/lang/go.mjs +194 -0
- package/src/parse/lang/jvm.mjs +118 -0
- package/src/parse/lang/kotlin.mjs +97 -0
- package/src/parse/lang/misc.mjs +115 -0
- package/src/parse/lang/native.mjs +126 -0
- package/src/parse/lang/python.mjs +153 -0
- package/src/parse/lang/rust.mjs +157 -0
- package/src/parse/lang/scripting.mjs +219 -0
- package/src/parse/languages.mjs +48 -0
- package/src/parse/runtime.mjs +56 -0
- package/src/query/check.mjs +345 -0
- package/src/query/facts.mjs +323 -0
- package/src/query/intel.mjs +683 -0
- package/src/query/read.mjs +81 -0
- package/src/query/testcmd.mjs +130 -0
- package/src/query/textsearch.mjs +178 -0
- package/src/search/search.mjs +210 -0
- package/src/search/tokenize.mjs +188 -0
- package/src/store/db.mjs +169 -0
- package/src/util/git.mjs +30 -0
- package/src/util/paths.mjs +63 -0
- package/src/util/regex.mjs +2 -0
- package/vendor/grammars/bash.wasm.gz +0 -0
- package/vendor/grammars/c.wasm.gz +0 -0
- package/vendor/grammars/c_sharp.wasm.gz +0 -0
- package/vendor/grammars/cpp.wasm.gz +0 -0
- package/vendor/grammars/css.wasm.gz +0 -0
- package/vendor/grammars/go.wasm.gz +0 -0
- package/vendor/grammars/java.wasm.gz +0 -0
- package/vendor/grammars/javascript.wasm.gz +0 -0
- package/vendor/grammars/kotlin.wasm.gz +0 -0
- package/vendor/grammars/manifest.json +148 -0
- package/vendor/grammars/php.wasm.gz +0 -0
- package/vendor/grammars/python.wasm.gz +0 -0
- package/vendor/grammars/ruby.wasm.gz +0 -0
- package/vendor/grammars/rust.wasm.gz +0 -0
- package/vendor/grammars/scala.wasm.gz +0 -0
- package/vendor/grammars/tsx.wasm.gz +0 -0
- package/vendor/grammars/typescript.wasm.gz +0 -0
- package/vendor/web-tree-sitter/web-tree-sitter.js +3980 -0
- package/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
- package/cli-ui.mjs +0 -78
- package/colbert.mjs +0 -168
- package/config.mjs +0 -494
- package/daemon-ctl.mjs +0 -175
- package/daemon-lock.mjs +0 -69
- package/embedders/python/mlx_embed_server.py +0 -61
- package/embedders/python/requirements-mlx.txt +0 -5
- package/embedders/setup-mlx.mjs +0 -110
- package/embeddings.mjs +0 -571
- package/engine/binary.mjs +0 -437
- package/engine/memory.mjs +0 -1003
- package/engine/sqlite.mjs +0 -1136
- package/enrichment.mjs +0 -521
- package/git-signals.mjs +0 -248
- package/indexer.mjs +0 -546
- package/init.mjs +0 -1809
- package/layout.mjs +0 -123
- package/mcp/centrality.mjs +0 -117
- package/mcp/format.mjs +0 -61
- package/mcp/resolver.mjs +0 -116
- package/mcp/symbolgraph.mjs +0 -137
- package/mcp/taint.mjs +0 -225
- package/mcp/tools.mjs +0 -1582
- package/mcp/topology.mjs +0 -676
- package/mcp-server.mjs +0 -197
- package/parse/extractor.mjs +0 -628
- package/parse/imports.mjs +0 -365
- package/parse/interprocedural.mjs +0 -134
- package/parse/languages.mjs +0 -253
- package/parse/metadata.mjs +0 -701
- package/parse/routes.mjs +0 -327
- package/parse/scip.mjs +0 -391
- package/parse/taint-patterns.mjs +0 -119
- package/prompts/CORE.md +0 -91
- package/prompts/DOMAIN_TEMPLATE.md +0 -46
- package/prompts/INTEGRATION.md +0 -123
- package/prompts/frameworks/ANDROID.md +0 -17
- package/prompts/frameworks/ASPNET_CORE.md +0 -19
- package/prompts/frameworks/FASTAPI_DJANGO.md +0 -20
- package/prompts/frameworks/LARAVEL_SYMFONY.md +0 -22
- package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +0 -19
- package/prompts/frameworks/RAILS.md +0 -20
- package/prompts/frameworks/REACT.md +0 -20
- package/prompts/frameworks/SPRING_BOOT.md +0 -20
- package/prompts/languages/BASH.md +0 -22
- package/prompts/languages/C.md +0 -21
- package/prompts/languages/CSHARP.md +0 -24
- package/prompts/languages/CSS_SCSS.md +0 -22
- package/prompts/languages/GO.md +0 -24
- package/prompts/languages/JAVA.md +0 -23
- package/prompts/languages/JAVASCRIPT_TYPESCRIPT.md +0 -24
- package/prompts/languages/KOTLIN.md +0 -24
- package/prompts/languages/PHP.md +0 -23
- package/prompts/languages/PYTHON.md +0 -23
- package/prompts/languages/RUBY.md +0 -23
- package/prompts/languages/RUST.md +0 -21
- package/prompts/languages/SWIFT.md +0 -21
- package/seal.mjs +0 -400
- package/search-core.mjs +0 -777
- package/search-sparse.mjs +0 -183
- package/storage.mjs +0 -73
- package/watch-daemon.mjs +0 -289
package/mcp/taint.mjs
DELETED
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file mcp/taint.mjs
|
|
3
|
-
* @description Taint analysis (C2): trace untrusted data from SOURCES (request bodies, argv/env,
|
|
4
|
-
* stdin, socket/file reads) to dangerous SINKS (eval/exec, SQL, fs/path, HTML, outbound
|
|
5
|
-
* requests) across the symbol graph, surfacing injection-class risks (rce/sqli/xss/
|
|
6
|
-
* path/ssrf) with a concrete source→sink path.
|
|
7
|
-
*
|
|
8
|
-
* QUERY-TIME and read-only: it scans chunk source with the heuristic catalogs in
|
|
9
|
-
* parse/taint-patterns.mjs and walks the call graph (A4 getEdges when present, else
|
|
10
|
-
* chunk.calls). No index-time cost, no serialization, no parity surface — the default
|
|
11
|
-
* index is byte-identical and the tools cost nothing unless invoked. Air-gapped (pure
|
|
12
|
-
* regex + graph traversal; no model, no network).
|
|
13
|
-
*
|
|
14
|
-
* HONESTY: this is a FINDER, not a verifier. It favours precision (direct same-function
|
|
15
|
-
* flows are 'high'; cross-function reachability is 'medium'; a sanitizer on the path
|
|
16
|
-
* lowers confidence) and will MISS flows through dynamic dispatch, reflection, ORM/
|
|
17
|
-
* query-builder indirection, and untyped collections. "0 findings" ≠ "no vulnerabilities."
|
|
18
|
-
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
19
|
-
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
20
|
-
* @license MIT
|
|
21
|
-
*/
|
|
22
|
-
import { SOURCES, SINKS, SANITIZERS, CATEGORY_SEVERITY, langKeyForExt } from '../parse/taint-patterns.mjs';
|
|
23
|
-
|
|
24
|
-
const extOf = (filePath) => { const i = String(filePath).lastIndexOf('.'); return i >= 0 ? filePath.slice(i) : ''; };
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Scan one chunk's source for source / sink / sanitizer hits. Returns null for unsupported
|
|
28
|
-
* languages or chunks without source text.
|
|
29
|
-
*/
|
|
30
|
-
export function scanChunk(chunk) {
|
|
31
|
-
const lang = langKeyForExt(extOf(chunk.file_path));
|
|
32
|
-
if (!lang) return null;
|
|
33
|
-
const src = chunk.code_snippet || '';
|
|
34
|
-
if (!src) return null;
|
|
35
|
-
const lines = src.split('\n');
|
|
36
|
-
const base = chunk.start_line || 1;
|
|
37
|
-
|
|
38
|
-
const sources = [];
|
|
39
|
-
const sinks = [];
|
|
40
|
-
for (let i = 0; i < lines.length; i++) {
|
|
41
|
-
const line = lines[i];
|
|
42
|
-
for (const s of SOURCES[lang]) if (s.re.test(line)) sources.push({ line: base + i, kind: s.kind, snippet: line.trim().slice(0, 160) });
|
|
43
|
-
for (const k of SINKS[lang]) if (k.re.test(line)) sinks.push({ line: base + i, category: k.category, label: k.label, snippet: line.trim().slice(0, 160) });
|
|
44
|
-
}
|
|
45
|
-
const sanitized = SANITIZERS[lang].some(re => re.test(src));
|
|
46
|
-
return { lang, sources, sinks, sanitized };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Forward callees of a chunk (what it calls): A4 edges when present, else resolved chunk.calls. */
|
|
50
|
-
function forwardCallees(db, chunk) {
|
|
51
|
-
const out = [];
|
|
52
|
-
const seen = new Set();
|
|
53
|
-
const push = (c) => { if (c && !seen.has(c.id)) { seen.add(c.id); out.push(c); } };
|
|
54
|
-
if (typeof db.hasSymbolGraph === 'function' && db.hasSymbolGraph()) {
|
|
55
|
-
for (const e of db.getEdges(chunk.id, { direction: 'out', kind: 'calls' })) if (e.chunk) push(e.chunk);
|
|
56
|
-
return out;
|
|
57
|
-
}
|
|
58
|
-
for (const name of (chunk.calls || [])) for (const def of (db.resolveSymbol(name) || [])) push(def);
|
|
59
|
-
return out;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const confRank = { high: 3, medium: 2, low: 1 };
|
|
63
|
-
const sevRank = Object.fromEntries(CATEGORY_SEVERITY.map((c, i) => [c, CATEGORY_SEVERITY.length - i]));
|
|
64
|
-
|
|
65
|
-
function flowKey(f) { return `${f.source.chunk_id}|${f.sink.chunk_id}|${f.sink.category}|${f.sink.line}`; }
|
|
66
|
-
|
|
67
|
-
/** Deterministic order: worst category, then confidence, then source place, then sink place, then id. */
|
|
68
|
-
function flowOrder(a, b) {
|
|
69
|
-
return (sevRank[b.sink.category] - sevRank[a.sink.category])
|
|
70
|
-
|| (confRank[b.confidence] - confRank[a.confidence])
|
|
71
|
-
|| a.source.file_path.localeCompare(b.source.file_path)
|
|
72
|
-
|| (a.source.line - b.source.line)
|
|
73
|
-
|| a.sink.file_path.localeCompare(b.sink.file_path)
|
|
74
|
-
|| (a.sink.line - b.sink.line)
|
|
75
|
-
|| (a.source.chunk_id < b.source.chunk_id ? -1 : a.source.chunk_id > b.source.chunk_id ? 1 : 0);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Build the taint flow set: every source→sink flow the heuristics can find.
|
|
80
|
-
*
|
|
81
|
-
* @param {object} db
|
|
82
|
-
* @param {object} [opts]
|
|
83
|
-
* @param {number} [opts.maxDepth] Cross-function call hops to follow from a source (default 4).
|
|
84
|
-
* @param {number} [opts.maxFlows] Cap on flows returned (default 200; truncation is reported).
|
|
85
|
-
* @param {string} [opts.category] Restrict to one sink category (rce|sqli|xss|path|ssrf).
|
|
86
|
-
* @param {boolean}[opts.includeReachable] Include cross-function reachability flows (default true).
|
|
87
|
-
* @returns {{ flows: object[], truncated: boolean, scanned: number, sources: number, sinks: number }}
|
|
88
|
-
*/
|
|
89
|
-
export function buildTaintGraph(db, { maxDepth = 4, maxFlows = 200, category = null, includeReachable = true } = {}) {
|
|
90
|
-
// ── Fast path: serialized flows from an index built with --taint (C2). The cache is the FULL
|
|
91
|
-
// flow set computed once at index time (includeReachable, uncapped), so any query within its
|
|
92
|
-
// depth envelope is served by filtering — no graph walk, and both backends serve identical
|
|
93
|
-
// flows. A direct-only query (includeReachable:false) or a deeper maxDepth than the cache
|
|
94
|
-
// falls through to a live recompute (correctness preserved over speed). ───────────────────
|
|
95
|
-
if (includeReachable && typeof db.hasTaint === 'function' && db.hasTaint()) {
|
|
96
|
-
const cached = db.getTaintFlows();
|
|
97
|
-
if (cached && cached.meta && maxDepth <= cached.meta.maxDepth) {
|
|
98
|
-
let flows = cached.flows || [];
|
|
99
|
-
if (category) flows = flows.filter(f => f.sink.category === category);
|
|
100
|
-
if (maxDepth < cached.meta.maxDepth) flows = flows.filter(f => f.depth <= maxDepth);
|
|
101
|
-
const truncated = flows.length > maxFlows;
|
|
102
|
-
return {
|
|
103
|
-
flows: truncated ? flows.slice(0, maxFlows) : flows, truncated, cached: true,
|
|
104
|
-
scanned: cached.meta.scanned, sources: cached.meta.sources, sinks: cached.meta.sinks,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Scan every chunk once (sorted by id for deterministic, backend-stable output).
|
|
110
|
-
const chunks = [...db.iterateChunks()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
111
|
-
const scans = new Map();
|
|
112
|
-
const sourceChunks = [];
|
|
113
|
-
const sinkChunkIds = new Set();
|
|
114
|
-
let sourceCount = 0, sinkCount = 0;
|
|
115
|
-
for (const c of chunks) {
|
|
116
|
-
const sc = scanChunk(c);
|
|
117
|
-
if (!sc) continue;
|
|
118
|
-
scans.set(c.id, sc);
|
|
119
|
-
if (sc.sources.length) { sourceChunks.push(c); sourceCount += sc.sources.length; }
|
|
120
|
-
if (sc.sinks.length) { sinkChunkIds.add(c.id); sinkCount += sc.sinks.length; }
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const sinkMatches = (sk) => !category || sk.category === category;
|
|
124
|
-
const card = (chunk) => ({ chunk_id: chunk.id, name: chunk.name, file_path: chunk.file_path });
|
|
125
|
-
|
|
126
|
-
const flows = new Map();
|
|
127
|
-
const addFlow = (srcChunk, srcHit, sinkChunk, sinkHit, path, depth, via, sanitized) => {
|
|
128
|
-
const confidence = via === 'direct' ? (sanitized ? 'low' : 'high') : (sanitized ? 'low' : 'medium');
|
|
129
|
-
const f = {
|
|
130
|
-
source: { ...card(srcChunk), line: srcHit.line, kind: srcHit.kind, snippet: srcHit.snippet },
|
|
131
|
-
sink: { ...card(sinkChunk), line: sinkHit.line, category: sinkHit.category, label: sinkHit.label, snippet: sinkHit.snippet },
|
|
132
|
-
path, depth, via, sanitized, confidence,
|
|
133
|
-
};
|
|
134
|
-
const k = flowKey(f);
|
|
135
|
-
const prev = flows.get(k);
|
|
136
|
-
if (!prev || confRank[f.confidence] > confRank[prev.confidence] || f.depth < prev.depth) flows.set(k, f);
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
// ── Direct: a single function that both takes untrusted input AND performs a dangerous op. ──
|
|
140
|
-
for (const c of sourceChunks) {
|
|
141
|
-
const sc = scans.get(c.id);
|
|
142
|
-
if (!sc.sinks.length) continue;
|
|
143
|
-
const srcHit = sc.sources[0]; // representative source
|
|
144
|
-
for (const sinkHit of sc.sinks) if (sinkMatches(sinkHit)) addFlow(c, srcHit, c, sinkHit, [c.id], 0, 'direct', sc.sanitized);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// ── Reachable: a source function transitively calls a sink function. ─────────────────────────
|
|
148
|
-
if (includeReachable) {
|
|
149
|
-
for (const start of sourceChunks) {
|
|
150
|
-
const startScan = scans.get(start.id);
|
|
151
|
-
const srcHit = startScan.sources[0];
|
|
152
|
-
const visited = new Set([start.id]);
|
|
153
|
-
let frontier = [{ chunk: start, path: [start.id], pathSanitized: startScan.sanitized }];
|
|
154
|
-
for (let depth = 1; depth <= maxDepth; depth++) {
|
|
155
|
-
const next = [];
|
|
156
|
-
for (const { chunk, path, pathSanitized } of frontier) {
|
|
157
|
-
for (const callee of forwardCallees(db, chunk)) {
|
|
158
|
-
if (visited.has(callee.id)) continue;
|
|
159
|
-
visited.add(callee.id);
|
|
160
|
-
const calleeScan = scans.get(callee.id);
|
|
161
|
-
const sanitizedPath = pathSanitized || (calleeScan ? calleeScan.sanitized : false);
|
|
162
|
-
if (sinkChunkIds.has(callee.id) && calleeScan) {
|
|
163
|
-
for (const sinkHit of calleeScan.sinks) if (sinkMatches(sinkHit)) addFlow(start, srcHit, callee, sinkHit, [...path, callee.id], depth, 'reachable', sanitizedPath);
|
|
164
|
-
}
|
|
165
|
-
next.push({ chunk: callee, path: [...path, callee.id], pathSanitized: sanitizedPath });
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
if (!next.length) break;
|
|
169
|
-
frontier = next;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const all = [...flows.values()].sort(flowOrder);
|
|
175
|
-
const truncated = all.length > maxFlows;
|
|
176
|
-
return { flows: truncated ? all.slice(0, maxFlows) : all, truncated, scanned: scans.size, sources: sourceCount, sinks: sinkCount };
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Compute the serializable taint cache (C2 --taint) for index-time serialization: the FULL flow set
|
|
181
|
-
* (uncapped, includeReachable) plus the meta the fast path needs to answer in-envelope queries
|
|
182
|
-
* (`maxDepth` is the cache's depth — a query asking for ≤ this is served from the cache).
|
|
183
|
-
*/
|
|
184
|
-
export function computeTaintCache(db, { maxDepth = 4 } = {}) {
|
|
185
|
-
const r = buildTaintGraph(db, { maxDepth, maxFlows: Infinity, includeReachable: true, category: null });
|
|
186
|
-
return { flows: r.flows, meta: { maxDepth, scanned: r.scanned, sources: r.sources, sinks: r.sinks } };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** Flows filtered by source kind and/or sink category (a thin filter over buildTaintGraph). */
|
|
190
|
-
export function traceTaint(db, { sourceKind = null, sinkCategory = null, maxDepth = 4, maxFlows = 200 } = {}) {
|
|
191
|
-
const { flows, truncated, scanned } = buildTaintGraph(db, { maxDepth, maxFlows, category: sinkCategory });
|
|
192
|
-
const filtered = sourceKind ? flows.filter(f => f.source.kind === sourceKind) : flows;
|
|
193
|
-
return { flows: filtered, truncated, scanned };
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Every dangerous sink, grouped by category, each annotated with whether any untrusted source
|
|
198
|
-
* reaches it. The orientation tool: "where are my dangerous operations, and which are reachable
|
|
199
|
-
* from untrusted input?"
|
|
200
|
-
*/
|
|
201
|
-
export function findTaintedSinks(db, { category = null, reachableOnly = false, maxDepth = 4 } = {}) {
|
|
202
|
-
const { flows, scanned } = buildTaintGraph(db, { maxDepth, maxFlows: 100000, category });
|
|
203
|
-
const reachedKey = new Set(flows.map(f => `${f.sink.chunk_id}|${f.sink.line}|${f.sink.category}`));
|
|
204
|
-
|
|
205
|
-
const chunks = [...db.iterateChunks()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
206
|
-
const byCategory = {};
|
|
207
|
-
for (const c of chunks) {
|
|
208
|
-
const sc = scanChunk(c);
|
|
209
|
-
if (!sc || !sc.sinks.length) continue;
|
|
210
|
-
for (const sk of sc.sinks) {
|
|
211
|
-
if (category && sk.category !== category) continue;
|
|
212
|
-
const reached = reachedKey.has(`${c.id}|${sk.line}|${sk.category}`);
|
|
213
|
-
if (reachableOnly && !reached) continue;
|
|
214
|
-
(byCategory[sk.category] ||= []).push({
|
|
215
|
-
chunk_id: c.id, name: c.name, file_path: c.file_path, line: sk.line,
|
|
216
|
-
label: sk.label, snippet: sk.snippet, reached_by_source: reached,
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
for (const cat of Object.keys(byCategory)) {
|
|
221
|
-
byCategory[cat].sort((a, b) => (Number(b.reached_by_source) - Number(a.reached_by_source))
|
|
222
|
-
|| a.file_path.localeCompare(b.file_path) || (a.line - b.line));
|
|
223
|
-
}
|
|
224
|
-
return { byCategory, scanned, flowCount: flows.length };
|
|
225
|
-
}
|