gitnexus 1.6.8-rc.22 → 1.6.8-rc.24
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/package.json +1 -1
- package/skills/gitnexus-taint-analysis.md +178 -0
|
@@ -49,6 +49,15 @@
|
|
|
49
49
|
*/
|
|
50
50
|
import { createKnowledgeGraph } from '../graph/graph.js';
|
|
51
51
|
const isGraphWide = (label) => label === 'Community' || label === 'Process';
|
|
52
|
+
/**
|
|
53
|
+
* Relationship types whose VALIDITY is a whole-program property, not a
|
|
54
|
+
* function of their endpoints' files (#2084 M4 U6). `TAINT_PATH` (cross-
|
|
55
|
+
* function taint) can be invalidated by a change to an INTERMEDIATE function
|
|
56
|
+
* on a third file, so the endpoint-writability rule below would skip a stale
|
|
57
|
+
* A→C edge. These are always extracted (and the orchestrator delete-alls them
|
|
58
|
+
* first, like Community/Process) so they rebuild from the fresh graph.
|
|
59
|
+
*/
|
|
60
|
+
const isGraphWideRelType = (type) => type === 'TAINT_PATH';
|
|
52
61
|
/**
|
|
53
62
|
* Build a Map<nodeId, filePath> for every File-bound node in the graph.
|
|
54
63
|
* Graph-wide nodes (Community/Process) have no filePath and are filtered.
|
|
@@ -74,7 +83,9 @@ export const extractChangedSubgraph = (fullGraph, toWriteSet) => {
|
|
|
74
83
|
}
|
|
75
84
|
});
|
|
76
85
|
fullGraph.forEachRelationship((r) => {
|
|
77
|
-
if (writableNodeIds.has(r.sourceId) ||
|
|
86
|
+
if (writableNodeIds.has(r.sourceId) ||
|
|
87
|
+
writableNodeIds.has(r.targetId) ||
|
|
88
|
+
isGraphWideRelType(r.type)) {
|
|
78
89
|
sub.addRelationship(r);
|
|
79
90
|
}
|
|
80
91
|
});
|
|
@@ -15,6 +15,7 @@ export { ormPhase, type ORMOutput } from './orm.js';
|
|
|
15
15
|
export { crossFilePhase, type CrossFileOutput } from './cross-file.js';
|
|
16
16
|
export { scopeResolutionPhase, type ScopeResolutionOutput, } from '../scope-resolution/pipeline/phase.js';
|
|
17
17
|
export { pruneLocalSymbolsPhase, type PruneLocalSymbolsOutput } from './prune-local-symbols.js';
|
|
18
|
+
export { taintSummariesPhase, type TaintSummariesOutput } from './taint-summaries.js';
|
|
18
19
|
export { mroPhase, type MROOutput } from './mro.js';
|
|
19
20
|
export { communitiesPhase, type CommunitiesOutput } from './communities.js';
|
|
20
21
|
export { processesPhase, type ProcessesOutput } from './processes.js';
|
|
@@ -16,6 +16,7 @@ export { ormPhase } from './orm.js';
|
|
|
16
16
|
export { crossFilePhase } from './cross-file.js';
|
|
17
17
|
export { scopeResolutionPhase, } from '../scope-resolution/pipeline/phase.js';
|
|
18
18
|
export { pruneLocalSymbolsPhase } from './prune-local-symbols.js';
|
|
19
|
+
export { taintSummariesPhase } from './taint-summaries.js';
|
|
19
20
|
export { mroPhase } from './mro.js';
|
|
20
21
|
export { communitiesPhase } from './communities.js';
|
|
21
22
|
export { processesPhase } from './processes.js';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase: taintSummaries (#2084 M4 U3/U5)
|
|
3
|
+
*
|
|
4
|
+
* The interprocedural taint fixpoint. Runs AFTER scope-resolution (where the
|
|
5
|
+
* complete, resolved `CALLS` graph lives in `ctx.graph` and the per-function
|
|
6
|
+
* summaries were harvested in-phase) and composes those summaries to find
|
|
7
|
+
* source→sink flows that cross function and file boundaries.
|
|
8
|
+
*
|
|
9
|
+
* Opt-in: registered with `enabledWhen: (o) => o.pdg === true` (the first real
|
|
10
|
+
* pdg-gated phase). A default `analyze` run never includes it, so the graph is
|
|
11
|
+
* byte-identical. No always-on phase depends on it (a filtered-out dep would
|
|
12
|
+
* throw in `getPhaseOutput`).
|
|
13
|
+
*
|
|
14
|
+
* @deps scopeResolution, pruneLocalSymbols
|
|
15
|
+
* @reads graph (CALLS edges, Function/Method nodes), scopeResolution output
|
|
16
|
+
* (functionSummaries)
|
|
17
|
+
* @writes graph (TAINT_PATH edges)
|
|
18
|
+
*/
|
|
19
|
+
import type { PipelinePhase } from './types.js';
|
|
20
|
+
export interface TaintSummariesOutput {
|
|
21
|
+
/** Function summaries fed to the fixpoint. */
|
|
22
|
+
summaries: number;
|
|
23
|
+
/** Cross-function findings (pre-cap). */
|
|
24
|
+
findings: number;
|
|
25
|
+
/** TAINT_PATH edges persisted. */
|
|
26
|
+
edgesEmitted: number;
|
|
27
|
+
/** Call sites whose callee did not resolve to a summary edge (diagnostics). */
|
|
28
|
+
unmatchedCallSites: number;
|
|
29
|
+
}
|
|
30
|
+
export declare const taintSummariesPhase: PipelinePhase<TaintSummariesOutput>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase: taintSummaries (#2084 M4 U3/U5)
|
|
3
|
+
*
|
|
4
|
+
* The interprocedural taint fixpoint. Runs AFTER scope-resolution (where the
|
|
5
|
+
* complete, resolved `CALLS` graph lives in `ctx.graph` and the per-function
|
|
6
|
+
* summaries were harvested in-phase) and composes those summaries to find
|
|
7
|
+
* source→sink flows that cross function and file boundaries.
|
|
8
|
+
*
|
|
9
|
+
* Opt-in: registered with `enabledWhen: (o) => o.pdg === true` (the first real
|
|
10
|
+
* pdg-gated phase). A default `analyze` run never includes it, so the graph is
|
|
11
|
+
* byte-identical. No always-on phase depends on it (a filtered-out dep would
|
|
12
|
+
* throw in `getPhaseOutput`).
|
|
13
|
+
*
|
|
14
|
+
* @deps scopeResolution, pruneLocalSymbols
|
|
15
|
+
* @reads graph (CALLS edges, Function/Method nodes), scopeResolution output
|
|
16
|
+
* (functionSummaries)
|
|
17
|
+
* @writes graph (TAINT_PATH edges)
|
|
18
|
+
*/
|
|
19
|
+
import { getPhaseOutput } from './types.js';
|
|
20
|
+
import { solveInterprocTaint, DEFAULT_MAX_INTERPROC_HOPS, DEFAULT_PDG_MAX_INTERPROC_FINDINGS, } from '../taint/interproc-solver.js';
|
|
21
|
+
import { emitInterprocTaint, DEFAULT_PDG_MAX_INTERPROC_EDGES } from '../taint/interproc-emit.js';
|
|
22
|
+
import { logger } from '../../logger.js';
|
|
23
|
+
const EMPTY = {
|
|
24
|
+
summaries: 0,
|
|
25
|
+
findings: 0,
|
|
26
|
+
edgesEmitted: 0,
|
|
27
|
+
unmatchedCallSites: 0,
|
|
28
|
+
};
|
|
29
|
+
export const taintSummariesPhase = {
|
|
30
|
+
name: 'taintSummaries',
|
|
31
|
+
deps: ['scopeResolution', 'pruneLocalSymbols'],
|
|
32
|
+
async execute(ctx, deps) {
|
|
33
|
+
const scope = getPhaseOutput(deps, 'scopeResolution');
|
|
34
|
+
const summaries = scope.functionSummaries;
|
|
35
|
+
if (summaries.length === 0)
|
|
36
|
+
return EMPTY;
|
|
37
|
+
// Index summaries by function node id.
|
|
38
|
+
const summaryMap = new Map(summaries.map((s) => [s.fnId, s]));
|
|
39
|
+
// Build the call-edge adjacency from resolved CALLS edges. The join to a
|
|
40
|
+
// summary's call-arg edge is by CALLEE NAME (base-independent — see the
|
|
41
|
+
// solver doc); recover it from the callee node's `name` property.
|
|
42
|
+
const callEdges = [];
|
|
43
|
+
for (const rel of ctx.graph.iterRelationshipsByType('CALLS')) {
|
|
44
|
+
const callee = ctx.graph.getNode(rel.targetId);
|
|
45
|
+
const calleeName = callee && typeof callee.properties.name === 'string' ? callee.properties.name : undefined;
|
|
46
|
+
if (calleeName === undefined)
|
|
47
|
+
continue;
|
|
48
|
+
callEdges.push({ callerId: rel.sourceId, calleeId: rel.targetId, calleeName });
|
|
49
|
+
}
|
|
50
|
+
// Arm the per-run caps (#2084 review P1-3) — every other pdg layer bounds
|
|
51
|
+
// its output via RepoMeta.pdg; without this the fixpoint state + TAINT_PATH
|
|
52
|
+
// edges grow unbounded on a fan-in-heavy repo (OOM). `0` ⇒ unlimited
|
|
53
|
+
// (preserved like the other pdg caps). The solver/emit already implement
|
|
54
|
+
// deterministic truncate-and-warn — this just hands them the budgets.
|
|
55
|
+
const maxFindings = ctx.options?.pdgMaxInterprocFindings ?? DEFAULT_PDG_MAX_INTERPROC_FINDINGS;
|
|
56
|
+
const maxHops = ctx.options?.pdgMaxInterprocHops ?? DEFAULT_MAX_INTERPROC_HOPS;
|
|
57
|
+
const maxEdges = ctx.options?.pdgMaxInterprocEdges ?? DEFAULT_PDG_MAX_INTERPROC_EDGES;
|
|
58
|
+
const solved = solveInterprocTaint(summaryMap, callEdges, { maxFindings, maxHops });
|
|
59
|
+
const emit = emitInterprocTaint(ctx.graph, solved.findings, { maxEdges }, (m) => logger.warn(m));
|
|
60
|
+
// Surface drops UNCONDITIONALLY (R4 — never silently truncate the layer).
|
|
61
|
+
if (solved.droppedFindings > 0 || emit.edgesDropped > 0) {
|
|
62
|
+
logger.warn(`[taint-interproc] capped: ${solved.droppedFindings} finding(s) dropped by the ` +
|
|
63
|
+
`per-run findings cap (${maxFindings}), ${emit.edgesDropped} edge(s) by the edge cap ` +
|
|
64
|
+
`(${maxEdges}) — raise pdgMaxInterprocFindings/pdgMaxInterprocEdges if intentional`);
|
|
65
|
+
}
|
|
66
|
+
if (solved.findings.length > 0 || emit.edgesEmitted > 0) {
|
|
67
|
+
logger.debug(`[taint-interproc] ${summaries.length} summaries, ${callEdges.length} CALLS edges → ` +
|
|
68
|
+
`${solved.findings.length} cross-function finding(s), ${emit.edgesEmitted} TAINT_PATH edge(s)` +
|
|
69
|
+
(emit.hopsTruncated > 0 ? `, ${emit.hopsTruncated} with truncated paths` : '') +
|
|
70
|
+
(solved.unmatchedCallSites > 0
|
|
71
|
+
? `, ${solved.unmatchedCallSites} unmatched call site(s)`
|
|
72
|
+
: ''));
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
summaries: summaries.length,
|
|
76
|
+
findings: solved.findings.length,
|
|
77
|
+
edgesEmitted: emit.edgesEmitted,
|
|
78
|
+
unmatchedCallSites: solved.unmatchedCallSites,
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
};
|
|
@@ -73,6 +73,19 @@ export interface PipelineOptions {
|
|
|
73
73
|
* no-CLI-flag discipline as `pdgMaxTaintFindingsPerFunction`.
|
|
74
74
|
*/
|
|
75
75
|
pdgMaxTaintHops?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Per-run cross-function findings cap (#2084 M4 review P1-3). `undefined` ⇒
|
|
78
|
+
* `DEFAULT_PDG_MAX_INTERPROC_FINDINGS` (2000); `0` ⇒ no cap. Consumed by the
|
|
79
|
+
* `taintSummaries` phase; RepoMeta-stamped, no CLI flag (KTD8) — same
|
|
80
|
+
* discipline as the per-function taint caps.
|
|
81
|
+
*/
|
|
82
|
+
pdgMaxInterprocFindings?: number;
|
|
83
|
+
/** Per-finding cross-function hop cap (#2084 review P1-3). `undefined` ⇒
|
|
84
|
+
* `DEFAULT_MAX_INTERPROC_HOPS` (32); `0` ⇒ no cap. */
|
|
85
|
+
pdgMaxInterprocHops?: number;
|
|
86
|
+
/** Per-run `TAINT_PATH` edge cap (#2084 review P1-3). `undefined` ⇒
|
|
87
|
+
* `DEFAULT_PDG_MAX_INTERPROC_EDGES` (1000); `0` ⇒ no cap. */
|
|
88
|
+
pdgMaxInterprocEdges?: number;
|
|
76
89
|
/**
|
|
77
90
|
* Request parsing with the worker pool disabled. The sequential parser was
|
|
78
91
|
* removed — the worker pool is the sole parse path — so setting this now
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* See ARCHITECTURE.md for the full phase dependency diagram.
|
|
16
16
|
*/
|
|
17
17
|
import { createKnowledgeGraph } from '../graph/graph.js';
|
|
18
|
-
import { runPipeline, getPhaseOutput, scanPhase, structurePhase, markdownPhase, cobolPhase, parsePhase, routesPhase, toolsPhase, ormPhase, crossFilePhase, scopeResolutionPhase, pruneLocalSymbolsPhase, mroPhase, communitiesPhase, processesPhase, PhaseRegistry, } from './pipeline-phases/index.js';
|
|
18
|
+
import { runPipeline, getPhaseOutput, scanPhase, structurePhase, markdownPhase, cobolPhase, parsePhase, routesPhase, toolsPhase, ormPhase, crossFilePhase, scopeResolutionPhase, pruneLocalSymbolsPhase, taintSummariesPhase, mroPhase, communitiesPhase, processesPhase, PhaseRegistry, } from './pipeline-phases/index.js';
|
|
19
19
|
// ── Phase registry ─────────────────────────────────────────────────────────
|
|
20
20
|
/**
|
|
21
21
|
* All pipeline phases with their dependency relationships.
|
|
@@ -49,6 +49,10 @@ export function buildPhaseList(options) {
|
|
|
49
49
|
.register(crossFilePhase)
|
|
50
50
|
.register(scopeResolutionPhase)
|
|
51
51
|
.register(pruneLocalSymbolsPhase)
|
|
52
|
+
// M4 (#2084): interprocedural taint fixpoint — the first real opt-in
|
|
53
|
+
// pdg-gated phase. Off ⇒ absent ⇒ byte-identical graph. No always-on
|
|
54
|
+
// phase depends on it (a filtered-out dep would throw in getPhaseOutput).
|
|
55
|
+
.register(taintSummariesPhase, { enabledWhen: (o) => o.pdg === true })
|
|
52
56
|
.register(mroPhase, { enabledWhen: (o) => !o.skipGraphPhases })
|
|
53
57
|
.register(communitiesPhase, { enabledWhen: (o) => !o.skipGraphPhases })
|
|
54
58
|
.register(processesPhase, { enabledWhen: (o) => !o.skipGraphPhases })
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import type { PipelinePhase } from '../../pipeline-phases/types.js';
|
|
27
27
|
import { SupportedLanguages } from '../../../../_shared/index.js';
|
|
28
28
|
import type { ResolutionOutcome } from '../resolution-outcome.js';
|
|
29
|
+
import type { FunctionSummary } from '../../taint/summary-model.js';
|
|
29
30
|
export interface ScopeResolutionOutput {
|
|
30
31
|
/** True when at least one language ran. */
|
|
31
32
|
readonly ran: boolean;
|
|
@@ -43,5 +44,11 @@ export interface ScopeResolutionOutput {
|
|
|
43
44
|
readonly importsEmitted: number;
|
|
44
45
|
readonly referenceEdgesEmitted: number;
|
|
45
46
|
}>;
|
|
47
|
+
/**
|
|
48
|
+
* Per-function taint summaries harvested in the pdg window (#2084 M4 U1),
|
|
49
|
+
* across all languages. Empty unless `--pdg` and a registered taint model.
|
|
50
|
+
* The `taintSummaries` phase composes these over the `CALLS` graph.
|
|
51
|
+
*/
|
|
52
|
+
readonly functionSummaries: readonly FunctionSummary[];
|
|
46
53
|
}
|
|
47
54
|
export declare const scopeResolutionPhase: PipelinePhase<ScopeResolutionOutput>;
|
|
@@ -33,6 +33,7 @@ import { SCOPE_RESOLVERS } from './registry.js';
|
|
|
33
33
|
import { isDev, isSemanticModelValidatorEnabled } from '../../utils/env.js';
|
|
34
34
|
import { logHeapProbe } from '../../utils/heap-probe.js';
|
|
35
35
|
import { clearParsedFileStore, loadParsedFilesForPaths, forceGc, } from '../../../../storage/parsedfile-store.js';
|
|
36
|
+
import { buildFunctionNodeIndex } from '../../taint/summary-harvest-driver.js';
|
|
36
37
|
import { logger } from '../../../logger.js';
|
|
37
38
|
const NOOP_OUTPUT = Object.freeze({
|
|
38
39
|
ran: false,
|
|
@@ -41,6 +42,7 @@ const NOOP_OUTPUT = Object.freeze({
|
|
|
41
42
|
referenceEdgesEmitted: 0,
|
|
42
43
|
resolutionOutcomes: [],
|
|
43
44
|
perLanguage: new Map(),
|
|
45
|
+
functionSummaries: [],
|
|
44
46
|
});
|
|
45
47
|
export const scopeResolutionPhase = {
|
|
46
48
|
name: 'scopeResolution',
|
|
@@ -103,6 +105,9 @@ export const scopeResolutionPhase = {
|
|
|
103
105
|
let totalRefs = 0;
|
|
104
106
|
let anyRan = false;
|
|
105
107
|
const resolutionOutcomes = [];
|
|
108
|
+
// M4 (#2084 U1): per-function taint summaries accumulated across every
|
|
109
|
+
// language pass; the cross-function fixpoint phase reads this output.
|
|
110
|
+
const functionSummaries = [];
|
|
106
111
|
const perLanguage = new Map();
|
|
107
112
|
// Pre-count files and languages for progress reporting. This avoids
|
|
108
113
|
// a frozen progress bar during long scope-resolution runs (#1741).
|
|
@@ -167,6 +172,13 @@ export const scopeResolutionPhase = {
|
|
|
167
172
|
logHeapProbe('scope-setup-nodeLookup-start', `langs=${totalScopeLangs} files=${totalScopeFiles}`);
|
|
168
173
|
const sharedNodeLookup = totalScopeFiles > 0 ? buildGraphNodeLookup(ctx.graph) : undefined;
|
|
169
174
|
logHeapProbe('scope-setup-nodeLookup-end', `langs=${totalScopeLangs}`);
|
|
175
|
+
// M4 (#2084 review P2-6): build the functionish-node index ONCE for the
|
|
176
|
+
// taint summary harvest, shared across every language pass (it is a whole-
|
|
177
|
+
// graph scan and language-agnostic). Only when pdg is on — off ⇒ undefined,
|
|
178
|
+
// no scan, byte-identical.
|
|
179
|
+
const sharedFnNodeIndex = ctx.options?.pdg === true && totalScopeFiles > 0
|
|
180
|
+
? buildFunctionNodeIndex(ctx.graph)
|
|
181
|
+
: undefined;
|
|
170
182
|
for (const [lang, provider] of SCOPE_RESOLVERS) {
|
|
171
183
|
// Standalone providers (COBOL, JCL) don't emit graph edges yet
|
|
172
184
|
// through the scope-resolution path. This is the canonical guard:
|
|
@@ -283,6 +295,7 @@ export const scopeResolutionPhase = {
|
|
|
283
295
|
files,
|
|
284
296
|
resolutionConfig,
|
|
285
297
|
prebuiltNodeLookup: sharedNodeLookup,
|
|
298
|
+
prebuiltFunctionNodeIndex: sharedFnNodeIndex,
|
|
286
299
|
preExtractedParsedFiles: preExtractedByPath,
|
|
287
300
|
scopeIndexStorePath: parsedFileStorePath,
|
|
288
301
|
// CFG/PDG emission (#2081 M1) — opt-in; off ⇒ byte-identical graph.
|
|
@@ -360,6 +373,7 @@ export const scopeResolutionPhase = {
|
|
|
360
373
|
logHeapProbe('scope-lang-end', `lang=${lang} filesProcessed=${stats.filesProcessed}`);
|
|
361
374
|
processedScopeFiles += langFileCount;
|
|
362
375
|
anyRan = true;
|
|
376
|
+
functionSummaries.push(...stats.functionSummaries);
|
|
363
377
|
totalFiles += stats.filesProcessed;
|
|
364
378
|
totalImports += stats.importsEmitted;
|
|
365
379
|
totalRefs += stats.referenceEdgesEmitted;
|
|
@@ -401,6 +415,7 @@ export const scopeResolutionPhase = {
|
|
|
401
415
|
referenceEdgesEmitted: totalRefs,
|
|
402
416
|
resolutionOutcomes,
|
|
403
417
|
perLanguage,
|
|
418
|
+
functionSummaries,
|
|
404
419
|
};
|
|
405
420
|
},
|
|
406
421
|
};
|
|
@@ -27,6 +27,8 @@ import type { KnowledgeGraph } from '../../../graph/types.js';
|
|
|
27
27
|
import type { MutableSemanticModel } from '../../model/semantic-model.js';
|
|
28
28
|
import { type ResolveStats } from '../../resolve-references.js';
|
|
29
29
|
import { buildGraphNodeLookup } from '../graph-bridge/node-lookup.js';
|
|
30
|
+
import { type FunctionNodeIndex } from '../../taint/summary-harvest-driver.js';
|
|
31
|
+
import type { FunctionSummary } from '../../taint/summary-model.js';
|
|
30
32
|
import type { ScopeResolver } from '../contract/scope-resolver.js';
|
|
31
33
|
import type { ResolutionOutcome, ResolutionOutcomeRecorder } from '../resolution-outcome.js';
|
|
32
34
|
export type ScopeResolutionSubPhase = 'extracting' | 'analyzing types' | 'resolving references' | 'linking symbols';
|
|
@@ -91,6 +93,14 @@ interface RunScopeResolutionInput {
|
|
|
91
93
|
* base is safe.
|
|
92
94
|
*/
|
|
93
95
|
readonly prebuiltNodeLookup?: ReturnType<typeof buildGraphNodeLookup>;
|
|
96
|
+
/**
|
|
97
|
+
* Functionish-node index built ONCE by the caller and shared across every
|
|
98
|
+
* language pass (#2084 review P2-6). Like `prebuiltNodeLookup`,
|
|
99
|
+
* `buildFunctionNodeIndex` is a whole-graph scan and is language-agnostic, so
|
|
100
|
+
* rebuilding it per language wastes a full scan each time. When omitted
|
|
101
|
+
* (tests / isolated calls) it is built locally for the pdg-enabled language.
|
|
102
|
+
*/
|
|
103
|
+
readonly prebuiltFunctionNodeIndex?: FunctionNodeIndex;
|
|
94
104
|
/**
|
|
95
105
|
* Opaque per-language import-resolution config (e.g. tsconfig path
|
|
96
106
|
* aliases for TypeScript). Loaded once by the caller via
|
|
@@ -148,6 +158,13 @@ interface RunScopeResolutionStats {
|
|
|
148
158
|
readonly referenceEdgesEmitted: number;
|
|
149
159
|
readonly referenceSkipped: number;
|
|
150
160
|
readonly resolutionOutcomes: readonly ResolutionOutcome[];
|
|
161
|
+
/**
|
|
162
|
+
* Per-function taint summaries harvested in the pdg window (#2084 M4 U1).
|
|
163
|
+
* Empty unless `input.pdg === true` and the language has a registered taint
|
|
164
|
+
* model. Keyed by resolved `Function`/`Method` node id; the cross-function
|
|
165
|
+
* fixpoint phase composes them over the complete `CALLS` graph.
|
|
166
|
+
*/
|
|
167
|
+
readonly functionSummaries: readonly FunctionSummary[];
|
|
151
168
|
}
|
|
152
169
|
export declare function runScopeResolution(input: RunScopeResolutionInput, provider: ScopeResolver): RunScopeResolutionStats;
|
|
153
170
|
export {};
|
|
@@ -30,10 +30,11 @@ import { extractParsedFile } from '../../scope-extractor-bridge.js';
|
|
|
30
30
|
import { finalizeScopeModel } from '../../finalize-orchestrator.js';
|
|
31
31
|
import { resolveReferenceSites } from '../../resolve-references.js';
|
|
32
32
|
import { buildGraphNodeLookup } from '../graph-bridge/node-lookup.js';
|
|
33
|
-
import { emitFileCfgs, emitFileReachingDefs, isEmitSafeCfg, DEFAULT_MAX_CFG_EDGES_PER_FUNCTION, DEFAULT_PDG_MAX_REACHING_DEF_EDGES_PER_FUNCTION, REACHING_DEF_FACTS_PER_EDGE_CAP, } from '../../cfg/emit.js';
|
|
33
|
+
import { emitFileCfgs, emitFileReachingDefs, isEmitSafeCfg, DEFAULT_MAX_CFG_EDGES_PER_FUNCTION, DEFAULT_PDG_MAX_REACHING_DEF_EDGES_PER_FUNCTION, DEFAULT_PDG_MAX_REACHING_DEF_FACTS_PER_FUNCTION, REACHING_DEF_FACTS_PER_EDGE_CAP, } from '../../cfg/emit.js';
|
|
34
34
|
import { emitFileTaint, DEFAULT_PDG_MAX_TAINT_FINDINGS_PER_FUNCTION, DEFAULT_PDG_MAX_TAINT_HOPS, } from '../../taint/emit.js';
|
|
35
35
|
import { registerBuiltinTaintModels } from '../../taint/typescript-model.js';
|
|
36
36
|
import { getSourceSinkConfig } from '../../taint/source-sink-registry.js';
|
|
37
|
+
import { buildFunctionNodeIndex, harvestFileSummaries, } from '../../taint/summary-harvest-driver.js';
|
|
37
38
|
import { resolveDefGraphId } from '../graph-bridge/ids.js';
|
|
38
39
|
import { buildPopulatedMethodDispatch } from '../graph-bridge/method-dispatch.js';
|
|
39
40
|
import { propagateImportedReturnTypes } from '../passes/imported-return-types.js';
|
|
@@ -290,6 +291,7 @@ export function runScopeResolution(input, provider) {
|
|
|
290
291
|
referenceEdgesEmitted: 0,
|
|
291
292
|
referenceSkipped: 0,
|
|
292
293
|
resolutionOutcomes,
|
|
294
|
+
functionSummaries: [],
|
|
293
295
|
};
|
|
294
296
|
}
|
|
295
297
|
const tExtract = PROF ? process.hrtime.bigint() : 0n;
|
|
@@ -471,6 +473,11 @@ export function runScopeResolution(input, provider) {
|
|
|
471
473
|
// pair can't bracket them; without this accumulator the M2 cost would
|
|
472
474
|
// silently disappear into `emit=` and field regressions would be invisible.
|
|
473
475
|
let pdgMs = 0;
|
|
476
|
+
// M4 (#2084 U1): per-function taint summaries harvested in the pdg window,
|
|
477
|
+
// returned on the stats for the cross-function fixpoint phase. Function-scoped
|
|
478
|
+
// so the return (below the pdg block) can read it; empty on non-pdg runs.
|
|
479
|
+
const harvestedSummaries = [];
|
|
480
|
+
let summaryUnresolved = 0;
|
|
474
481
|
// M3 (#2083 U4): accumulated taint time (match + taint-side solve +
|
|
475
482
|
// propagate + TAINTED/SANITIZES emit), a sibling of `pdgMs` for the same
|
|
476
483
|
// reason — it interleaves per file inside `emit=`, so only an accumulator
|
|
@@ -523,6 +530,13 @@ export function runScopeResolution(input, provider) {
|
|
|
523
530
|
gapExamples: [],
|
|
524
531
|
dropExamples: [],
|
|
525
532
|
};
|
|
533
|
+
// M4 (#2084 U1): per-function summary harvest. The functionish-node index
|
|
534
|
+
// is built ONCE (whole-graph scan) and reused across every file; summaries
|
|
535
|
+
// accumulate here and ride out on the stats for the cross-function fixpoint
|
|
536
|
+
// phase. Only built when the language has a registered taint model.
|
|
537
|
+
const fnNodeIndex = taintSpec !== undefined
|
|
538
|
+
? (input.prebuiltFunctionNodeIndex ?? buildFunctionNodeIndex(graph))
|
|
539
|
+
: undefined;
|
|
526
540
|
for (const pf of emitParsedFiles) {
|
|
527
541
|
const cfgs = pf.cfgSideChannel;
|
|
528
542
|
// Defensive: cfgSideChannel is opaque (`unknown`) and crosses the cache /
|
|
@@ -595,6 +609,19 @@ export function runScopeResolution(input, provider) {
|
|
|
595
609
|
if (taintTotals.dropExamples.length < 5)
|
|
596
610
|
taintTotals.dropExamples.push(ex);
|
|
597
611
|
}
|
|
612
|
+
// M4 (#2084 U1): harvest per-function summaries over the SAME
|
|
613
|
+
// emit-safe CFGs, inside the SAME per-file try. Pure aside from the
|
|
614
|
+
// read-only node-index lookup; the cross-function fixpoint phase
|
|
615
|
+
// consumes `harvestedSummaries` once the whole call graph is built.
|
|
616
|
+
if (fnNodeIndex !== undefined) {
|
|
617
|
+
const harvest = harvestFileSummaries(fnNodeIndex, wellFormed, pf.parsedImports, taintSpec,
|
|
618
|
+
// Same fact cap the taint-side RD solve uses (coverage parity).
|
|
619
|
+
taintLimits.maxFacts && taintLimits.maxFacts > 0
|
|
620
|
+
? taintLimits.maxFacts
|
|
621
|
+
: DEFAULT_PDG_MAX_REACHING_DEF_FACTS_PER_FUNCTION);
|
|
622
|
+
harvestedSummaries.push(...harvest.summaries);
|
|
623
|
+
summaryUnresolved += harvest.unresolved;
|
|
624
|
+
}
|
|
598
625
|
}
|
|
599
626
|
}
|
|
600
627
|
catch (err) {
|
|
@@ -657,6 +684,14 @@ export function runScopeResolution(input, provider) {
|
|
|
657
684
|
logger.warn(`[taint] lang=${provider.language}: ${parts.join('; ')}`);
|
|
658
685
|
}
|
|
659
686
|
}
|
|
687
|
+
// M4 (#2084 U1): summary harvest volume + anchor-resolution diagnostics.
|
|
688
|
+
if (harvestedSummaries.length > 0 || summaryUnresolved > 0) {
|
|
689
|
+
logger.debug(`[taint-summary] lang=${provider.language}: ${harvestedSummaries.length} function ` +
|
|
690
|
+
`summary/summaries harvested` +
|
|
691
|
+
(summaryUnresolved > 0
|
|
692
|
+
? `, ${summaryUnresolved} CFG anchor(s) unresolved (same-line collision or missing node)`
|
|
693
|
+
: ''));
|
|
694
|
+
}
|
|
660
695
|
}
|
|
661
696
|
if (PROF) {
|
|
662
697
|
const tEnd = process.hrtime.bigint();
|
|
@@ -681,5 +716,6 @@ export function runScopeResolution(input, provider) {
|
|
|
681
716
|
referenceEdgesEmitted: emitted + receiverExtras + unresolvedReceiverExtras + freeCallExtras,
|
|
682
717
|
referenceSkipped: skipped,
|
|
683
718
|
resolutionOutcomes,
|
|
719
|
+
functionSummaries: harvestedSummaries,
|
|
684
720
|
};
|
|
685
721
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interprocedural taint emission (#2084 M4 U4) — materialise `TAINT_PATH`.
|
|
3
|
+
*
|
|
4
|
+
* Persists each cross-function {@link InterprocFinding} as ONE `TAINT_PATH`
|
|
5
|
+
* edge from the source function node to the sink function node, with the
|
|
6
|
+
* function-level hop chain + sink kind encoded in `reason` via the SHARED
|
|
7
|
+
* `path-codec` (the same versioned wire format M3's intra-procedural `TAINTED`
|
|
8
|
+
* edges use — never a second hand-rolled codec). The MCP `explain` tool decodes
|
|
9
|
+
* it for cross-function path rendering (U7).
|
|
10
|
+
*
|
|
11
|
+
* `TAINT_PATH` was reserved at M0 (the RelationshipType + the `CodeRelation`
|
|
12
|
+
* Function/Method node-pairs already exist), so materialisation needs zero
|
|
13
|
+
* schema work. Like `TAINTED`, it stays out of `VALID_RELATION_TYPES` and the
|
|
14
|
+
* web schema — `explain` is the discovery surface.
|
|
15
|
+
*
|
|
16
|
+
* Boundedness mirrors the M3 emit driver: dedup-before-cap (the solver already
|
|
17
|
+
* deduped by `(source, sink, kind)`), a per-run findings cap, and unconditional
|
|
18
|
+
* truncate-and-warn — never a silent drop.
|
|
19
|
+
*/
|
|
20
|
+
import type { KnowledgeGraph } from '../../graph/types.js';
|
|
21
|
+
import type { InterprocFinding } from './interproc-solver.js';
|
|
22
|
+
/** Confidence stamped on interprocedural `TAINT_PATH` edges. Lower than the
|
|
23
|
+
* intra-procedural `TAINTED` 1.0 — context-insensitive composition is a
|
|
24
|
+
* coarser signal (return/call-site merging). */
|
|
25
|
+
export declare const INTERPROC_TAINT_CONFIDENCE = 0.6;
|
|
26
|
+
/**
|
|
27
|
+
* Default per-run cap on emitted `TAINT_PATH` edges (#2084 review P1-3).
|
|
28
|
+
* Resolved into `RepoMeta.pdg` like the other pdg caps; `0` ⇒ unlimited.
|
|
29
|
+
*/
|
|
30
|
+
export declare const DEFAULT_PDG_MAX_INTERPROC_EDGES = 1000;
|
|
31
|
+
export interface InterprocEmitLimits {
|
|
32
|
+
/** Max `TAINT_PATH` edges per run (post-dedup). `undefined`/0 ⇒ unlimited. */
|
|
33
|
+
readonly maxEdges?: number;
|
|
34
|
+
}
|
|
35
|
+
export interface InterprocEmitResult {
|
|
36
|
+
/** TAINT_PATH edges persisted. */
|
|
37
|
+
edgesEmitted: number;
|
|
38
|
+
/** Findings dropped by the per-run cap. */
|
|
39
|
+
edgesDropped: number;
|
|
40
|
+
/** Findings whose persisted hop path is a truncated prefix. */
|
|
41
|
+
hopsTruncated: number;
|
|
42
|
+
/** Findings skipped because an endpoint node was missing from the graph. */
|
|
43
|
+
skippedMissingEndpoint: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Persist cross-function findings as `TAINT_PATH` edges. `findings` is assumed
|
|
47
|
+
* deduped + deterministically ordered (the solver's contract). Never throws on
|
|
48
|
+
* valid input.
|
|
49
|
+
*/
|
|
50
|
+
export declare function emitInterprocTaint(graph: KnowledgeGraph, findings: readonly InterprocFinding[], limits?: InterprocEmitLimits, onWarn?: (message: string) => void): InterprocEmitResult;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interprocedural taint emission (#2084 M4 U4) — materialise `TAINT_PATH`.
|
|
3
|
+
*
|
|
4
|
+
* Persists each cross-function {@link InterprocFinding} as ONE `TAINT_PATH`
|
|
5
|
+
* edge from the source function node to the sink function node, with the
|
|
6
|
+
* function-level hop chain + sink kind encoded in `reason` via the SHARED
|
|
7
|
+
* `path-codec` (the same versioned wire format M3's intra-procedural `TAINTED`
|
|
8
|
+
* edges use — never a second hand-rolled codec). The MCP `explain` tool decodes
|
|
9
|
+
* it for cross-function path rendering (U7).
|
|
10
|
+
*
|
|
11
|
+
* `TAINT_PATH` was reserved at M0 (the RelationshipType + the `CodeRelation`
|
|
12
|
+
* Function/Method node-pairs already exist), so materialisation needs zero
|
|
13
|
+
* schema work. Like `TAINTED`, it stays out of `VALID_RELATION_TYPES` and the
|
|
14
|
+
* web schema — `explain` is the discovery surface.
|
|
15
|
+
*
|
|
16
|
+
* Boundedness mirrors the M3 emit driver: dedup-before-cap (the solver already
|
|
17
|
+
* deduped by `(source, sink, kind)`), a per-run findings cap, and unconditional
|
|
18
|
+
* truncate-and-warn — never a silent drop.
|
|
19
|
+
*/
|
|
20
|
+
import { encodeTaintPath } from './path-codec.js';
|
|
21
|
+
/** Confidence stamped on interprocedural `TAINT_PATH` edges. Lower than the
|
|
22
|
+
* intra-procedural `TAINTED` 1.0 — context-insensitive composition is a
|
|
23
|
+
* coarser signal (return/call-site merging). */
|
|
24
|
+
export const INTERPROC_TAINT_CONFIDENCE = 0.6;
|
|
25
|
+
/**
|
|
26
|
+
* Default per-run cap on emitted `TAINT_PATH` edges (#2084 review P1-3).
|
|
27
|
+
* Resolved into `RepoMeta.pdg` like the other pdg caps; `0` ⇒ unlimited.
|
|
28
|
+
*/
|
|
29
|
+
export const DEFAULT_PDG_MAX_INTERPROC_EDGES = 1000;
|
|
30
|
+
/**
|
|
31
|
+
* Persist cross-function findings as `TAINT_PATH` edges. `findings` is assumed
|
|
32
|
+
* deduped + deterministically ordered (the solver's contract). Never throws on
|
|
33
|
+
* valid input.
|
|
34
|
+
*/
|
|
35
|
+
export function emitInterprocTaint(graph, findings, limits, onWarn) {
|
|
36
|
+
const result = {
|
|
37
|
+
edgesEmitted: 0,
|
|
38
|
+
edgesDropped: 0,
|
|
39
|
+
hopsTruncated: 0,
|
|
40
|
+
skippedMissingEndpoint: 0,
|
|
41
|
+
};
|
|
42
|
+
const maxEdges = limits?.maxEdges && limits.maxEdges > 0 ? limits.maxEdges : Infinity;
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
for (const finding of findings) {
|
|
45
|
+
if (result.edgesEmitted >= maxEdges) {
|
|
46
|
+
result.edgesDropped++;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const sourceNode = graph.getNode(finding.sourceFnId);
|
|
50
|
+
const sinkNode = graph.getNode(finding.sinkFnId);
|
|
51
|
+
if (!sourceNode || !sinkNode) {
|
|
52
|
+
result.skippedMissingEndpoint++;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
// Map function hops → codec hops. The hop "name" is the function's display
|
|
56
|
+
// name (identifier charset — codec-safe); the line is its start line.
|
|
57
|
+
const hops = finding.hops.map((h) => {
|
|
58
|
+
const node = graph.getNode(h.fnId);
|
|
59
|
+
const name = typeof node?.properties.name === 'string' ? node.properties.name : 'fn';
|
|
60
|
+
const line = typeof node?.properties.startLine === 'number' ? node.properties.startLine : 0;
|
|
61
|
+
return { name, line };
|
|
62
|
+
});
|
|
63
|
+
const encoded = encodeTaintPath(hops, {
|
|
64
|
+
kind: finding.sinkKind,
|
|
65
|
+
truncated: finding.hopsTruncated,
|
|
66
|
+
});
|
|
67
|
+
if (encoded.truncated)
|
|
68
|
+
result.hopsTruncated++;
|
|
69
|
+
const id = `rel:TAINT_PATH:${finding.sinkKind}:${finding.sourceFnId}=>${finding.sinkFnId}`;
|
|
70
|
+
if (seen.has(id))
|
|
71
|
+
continue;
|
|
72
|
+
seen.add(id);
|
|
73
|
+
graph.addRelationship({
|
|
74
|
+
id,
|
|
75
|
+
sourceId: finding.sourceFnId,
|
|
76
|
+
targetId: finding.sinkFnId,
|
|
77
|
+
type: 'TAINT_PATH',
|
|
78
|
+
confidence: INTERPROC_TAINT_CONFIDENCE,
|
|
79
|
+
reason: encoded.reason,
|
|
80
|
+
});
|
|
81
|
+
result.edgesEmitted++;
|
|
82
|
+
}
|
|
83
|
+
if (result.edgesDropped > 0) {
|
|
84
|
+
onWarn?.(`[taint-interproc] ${result.edgesDropped} cross-function finding(s) dropped by the ` +
|
|
85
|
+
`per-run TAINT_PATH cap (${maxEdges})`);
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|