blast-radius-analyzer 1.2.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/README.md +108 -0
- package/TEST-REPORT.md +379 -0
- package/dist/core/AnalysisCache.d.ts +59 -0
- package/dist/core/AnalysisCache.js +156 -0
- package/dist/core/BlastRadiusAnalyzer.d.ts +99 -0
- package/dist/core/BlastRadiusAnalyzer.js +510 -0
- package/dist/core/CallStackBuilder.d.ts +63 -0
- package/dist/core/CallStackBuilder.js +269 -0
- package/dist/core/DataFlowAnalyzer.d.ts +215 -0
- package/dist/core/DataFlowAnalyzer.js +1115 -0
- package/dist/core/DependencyGraph.d.ts +55 -0
- package/dist/core/DependencyGraph.js +541 -0
- package/dist/core/ImpactTracer.d.ts +96 -0
- package/dist/core/ImpactTracer.js +398 -0
- package/dist/core/PropagationTracker.d.ts +73 -0
- package/dist/core/PropagationTracker.js +502 -0
- package/dist/core/PropertyAccessTracker.d.ts +56 -0
- package/dist/core/PropertyAccessTracker.js +281 -0
- package/dist/core/SymbolAnalyzer.d.ts +139 -0
- package/dist/core/SymbolAnalyzer.js +608 -0
- package/dist/core/TypeFlowAnalyzer.d.ts +120 -0
- package/dist/core/TypeFlowAnalyzer.js +654 -0
- package/dist/core/TypePropagationAnalyzer.d.ts +58 -0
- package/dist/core/TypePropagationAnalyzer.js +269 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +952 -0
- package/dist/types.d.ts +102 -0
- package/dist/types.js +5 -0
- package/package.json +39 -0
- package/src/core/AnalysisCache.ts +189 -0
- package/src/core/CallStackBuilder.ts +345 -0
- package/src/core/DataFlowAnalyzer.ts +1403 -0
- package/src/core/DependencyGraph.ts +584 -0
- package/src/core/ImpactTracer.ts +521 -0
- package/src/core/PropagationTracker.ts +630 -0
- package/src/core/PropertyAccessTracker.ts +349 -0
- package/src/core/SymbolAnalyzer.ts +746 -0
- package/src/core/TypeFlowAnalyzer.ts +844 -0
- package/src/core/TypePropagationAnalyzer.ts +332 -0
- package/src/index.ts +1071 -0
- package/src/types.ts +163 -0
- package/test-cases/.blast-radius-cache/file-states.json +14 -0
- package/test-cases/config.ts +13 -0
- package/test-cases/consumer.ts +12 -0
- package/test-cases/nested.ts +25 -0
- package/test-cases/simple.ts +62 -0
- package/test-cases/tsconfig.json +11 -0
- package/test-cases/user.ts +32 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,1403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commercial-Grade Data Flow Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Implements:
|
|
5
|
+
* - Interprocedural data flow analysis (cross-function tracking)
|
|
6
|
+
* - Control flow sensitivity (branches, loops, exceptions)
|
|
7
|
+
* - Path sensitivity (different branches = different type states)
|
|
8
|
+
* - Context sensitivity (same function, different call sites = different types)
|
|
9
|
+
* - Worklist algorithm with fixed-point computation
|
|
10
|
+
* - Lattice-based abstract interpretation
|
|
11
|
+
* - Symbolic execution for branch conditions
|
|
12
|
+
* - Points-to analysis for reference tracking
|
|
13
|
+
* - Taint analysis for security-sensitive data
|
|
14
|
+
* - Escape analysis for closure/global escape
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as ts from 'typescript';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
|
|
20
|
+
// === LATTICE TYPES ===
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Abstract value in the data flow lattice
|
|
24
|
+
*/
|
|
25
|
+
export interface AbstractValue {
|
|
26
|
+
/** Type representation */
|
|
27
|
+
type: string;
|
|
28
|
+
/** Possible values (constants) */
|
|
29
|
+
constants: Set<string>;
|
|
30
|
+
/** Is null/undefined possible */
|
|
31
|
+
nullable: boolean;
|
|
32
|
+
/** Property types if object */
|
|
33
|
+
properties: Map<string, AbstractValue>;
|
|
34
|
+
/** Array element type if array */
|
|
35
|
+
elementType?: AbstractValue;
|
|
36
|
+
/** Is this value tainted (user input, etc.) */
|
|
37
|
+
tainted: boolean;
|
|
38
|
+
/** Where did this value escape (closure, global, return) */
|
|
39
|
+
escapes: Set<'closure' | 'global' | 'parameter' | 'return'>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Data flow fact at a program point
|
|
44
|
+
*/
|
|
45
|
+
export interface DataFlowFact {
|
|
46
|
+
/** Variable name -> abstract value */
|
|
47
|
+
env: Map<string, AbstractValue>;
|
|
48
|
+
/** Type constraints */
|
|
49
|
+
constraints: TypeConstraint[];
|
|
50
|
+
/** Path condition (branch predicates) */
|
|
51
|
+
pathCondition: PathCondition[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Type constraint
|
|
56
|
+
*/
|
|
57
|
+
export interface TypeConstraint {
|
|
58
|
+
variable: string;
|
|
59
|
+
predicate: string;
|
|
60
|
+
thenTypes?: Map<string, AbstractValue>;
|
|
61
|
+
elseTypes?: Map<string, AbstractValue>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Path condition from branch predicates
|
|
66
|
+
*/
|
|
67
|
+
export interface PathCondition {
|
|
68
|
+
expression: string;
|
|
69
|
+
/** true = then branch, false = else branch */
|
|
70
|
+
polarity: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Basic block in CFG
|
|
75
|
+
*/
|
|
76
|
+
interface BasicBlock {
|
|
77
|
+
id: string;
|
|
78
|
+
statements: ts.Statement[];
|
|
79
|
+
predecessors: string[];
|
|
80
|
+
successors: string[];
|
|
81
|
+
/** Branch info if this block ends with a branch */
|
|
82
|
+
branch?: {
|
|
83
|
+
condition: ts.Expression;
|
|
84
|
+
trueTarget: string;
|
|
85
|
+
falseTarget: string;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Control Flow Graph
|
|
91
|
+
*/
|
|
92
|
+
interface ControlFlowGraph {
|
|
93
|
+
blocks: Map<string, BasicBlock>;
|
|
94
|
+
entryBlock: string;
|
|
95
|
+
exitBlock: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Call site information
|
|
100
|
+
*/
|
|
101
|
+
interface CallSite {
|
|
102
|
+
callee: ts.FunctionDeclaration | ts.ArrowFunction | ts.MethodDeclaration;
|
|
103
|
+
arguments: Map<string, DataFlowFact>;
|
|
104
|
+
returnVariable?: string;
|
|
105
|
+
callExpression: ts.CallExpression;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Analysis result with full flow paths
|
|
110
|
+
*/
|
|
111
|
+
export interface DataFlowResult {
|
|
112
|
+
hasDataLeaks: boolean;
|
|
113
|
+
flowPaths: FlowPath[];
|
|
114
|
+
taintedPaths: TaintedPath[];
|
|
115
|
+
typeNarrowing: Map<string, { line: number; types: string[] }[]>;
|
|
116
|
+
statistics: {
|
|
117
|
+
nodesAnalyzed: number;
|
|
118
|
+
blocksConstructed: number;
|
|
119
|
+
callSitesAnalyzed: number;
|
|
120
|
+
fixedPointIterations: number;
|
|
121
|
+
constraintsGenerated: number;
|
|
122
|
+
typesNarrowed: number;
|
|
123
|
+
promiseUnwraps: number;
|
|
124
|
+
conditionalBranches: number;
|
|
125
|
+
escapedValues: number;
|
|
126
|
+
taintedValues: number;
|
|
127
|
+
pathsTracked: number;
|
|
128
|
+
};
|
|
129
|
+
confidence: 'high' | 'medium' | 'low';
|
|
130
|
+
duration: number;
|
|
131
|
+
/** All facts at exit of each block */
|
|
132
|
+
finalFacts: Map<string, DataFlowFact>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface FlowPath {
|
|
136
|
+
source: string;
|
|
137
|
+
sink: string;
|
|
138
|
+
path: string[];
|
|
139
|
+
typeAtSink: string;
|
|
140
|
+
typeAtSource: string;
|
|
141
|
+
isTainted: boolean;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface TaintedPath {
|
|
145
|
+
source: string;
|
|
146
|
+
sink: string;
|
|
147
|
+
taintSource: 'user-input' | 'file-read' | 'network' | 'environment';
|
|
148
|
+
path: string[];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export class DataFlowAnalyzer {
|
|
152
|
+
private program: ts.Program;
|
|
153
|
+
private checker: ts.TypeChecker;
|
|
154
|
+
private sourceFiles: ts.SourceFile[] = [];
|
|
155
|
+
private cfgCache: Map<string, ControlFlowGraph> = new Map();
|
|
156
|
+
private worklist: string[] = [];
|
|
157
|
+
private analyzedCallSites: Set<string> = new Set();
|
|
158
|
+
|
|
159
|
+
// Analysis options
|
|
160
|
+
private maxIterations = 100;
|
|
161
|
+
private maxCallDepth = 5;
|
|
162
|
+
private trackTaint = true;
|
|
163
|
+
private trackEscapes = true;
|
|
164
|
+
|
|
165
|
+
constructor(projectRoot: string, tsConfigPath: string) {
|
|
166
|
+
const configFile = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
|
|
167
|
+
const parsedConfig = ts.parseJsonConfigFileContent(
|
|
168
|
+
configFile.config,
|
|
169
|
+
ts.sys,
|
|
170
|
+
path.dirname(tsConfigPath)
|
|
171
|
+
);
|
|
172
|
+
this.program = ts.createProgram(parsedConfig.fileNames, parsedConfig.options);
|
|
173
|
+
this.checker = this.program.getTypeChecker();
|
|
174
|
+
this.sourceFiles = this.program.getSourceFiles().filter(
|
|
175
|
+
sf => !sf.fileName.includes('node_modules')
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* MAIN ENTRY POINT - Full interprocedural data flow analysis
|
|
181
|
+
*/
|
|
182
|
+
analyzeDataFlow(functionName: string, functionFile: string): DataFlowResult {
|
|
183
|
+
const startTime = Date.now();
|
|
184
|
+
const stats = {
|
|
185
|
+
nodesAnalyzed: 0,
|
|
186
|
+
blocksConstructed: 0,
|
|
187
|
+
callSitesAnalyzed: 0,
|
|
188
|
+
fixedPointIterations: 0,
|
|
189
|
+
constraintsGenerated: 0,
|
|
190
|
+
typesNarrowed: 0,
|
|
191
|
+
promiseUnwraps: 0,
|
|
192
|
+
conditionalBranches: 0,
|
|
193
|
+
escapedValues: 0,
|
|
194
|
+
taintedValues: 0,
|
|
195
|
+
pathsTracked: 0,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const finalFacts = new Map<string, DataFlowFact>();
|
|
199
|
+
const flowPaths: FlowPath[] = [];
|
|
200
|
+
const taintedPaths: TaintedPath[] = [];
|
|
201
|
+
const typeNarrowing = new Map<string, { line: number; types: string[] }[]>();
|
|
202
|
+
|
|
203
|
+
// 1. Find the target function
|
|
204
|
+
const targetFunc = this.findFunction(functionName, functionFile);
|
|
205
|
+
if (!targetFunc) {
|
|
206
|
+
return this.createEmptyResult('low', stats, Date.now() - startTime);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 2. Build CFG for the function
|
|
210
|
+
const cfg = this.buildCFG(targetFunc);
|
|
211
|
+
stats.blocksConstructed = cfg.blocks.size;
|
|
212
|
+
|
|
213
|
+
// 3. Initialize entry fact (empty environment with parameters)
|
|
214
|
+
const entryFact = this.createEntryFact(targetFunc);
|
|
215
|
+
|
|
216
|
+
// 4. Run worklist algorithm with lattice-based fixed-point computation
|
|
217
|
+
const blockFacts = this.runWorklistAnalysis(cfg, entryFact, stats);
|
|
218
|
+
|
|
219
|
+
// 5. Extract flow paths from final facts
|
|
220
|
+
for (const [blockId, fact] of blockFacts) {
|
|
221
|
+
finalFacts.set(blockId, fact);
|
|
222
|
+
|
|
223
|
+
// Collect type narrowing
|
|
224
|
+
for (const [varName, value] of fact.env) {
|
|
225
|
+
if (value.constants.size > 1) {
|
|
226
|
+
if (!typeNarrowing.has(varName)) {
|
|
227
|
+
typeNarrowing.set(varName, []);
|
|
228
|
+
}
|
|
229
|
+
typeNarrowing.get(varName)!.push({
|
|
230
|
+
line: 0,
|
|
231
|
+
types: Array.from(value.constants),
|
|
232
|
+
});
|
|
233
|
+
stats.typesNarrowed++;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Collect tainted paths
|
|
238
|
+
if (this.trackTaint) {
|
|
239
|
+
for (const [varName, value] of fact.env) {
|
|
240
|
+
if (value.tainted) {
|
|
241
|
+
stats.taintedValues++;
|
|
242
|
+
taintedPaths.push({
|
|
243
|
+
source: `tainted:${varName}`,
|
|
244
|
+
sink: `${varName} at block ${blockId}`,
|
|
245
|
+
taintSource: 'user-input',
|
|
246
|
+
path: [varName],
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Collect escapes
|
|
253
|
+
if (this.trackEscapes) {
|
|
254
|
+
for (const [varName, value] of fact.env) {
|
|
255
|
+
if (value.escapes.size > 0) {
|
|
256
|
+
stats.escapedValues++;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 6. Analyze Promise/async patterns
|
|
263
|
+
this.analyzeAsyncPatterns(targetFunc, flowPaths, stats);
|
|
264
|
+
|
|
265
|
+
// 7. Check for data leaks (tainted -> return/escape)
|
|
266
|
+
const hasDataLeaks = this.checkDataLeaks(flowPaths, taintedPaths);
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
hasDataLeaks,
|
|
270
|
+
flowPaths,
|
|
271
|
+
taintedPaths,
|
|
272
|
+
typeNarrowing,
|
|
273
|
+
statistics: stats,
|
|
274
|
+
confidence: this.calculateConfidence(stats),
|
|
275
|
+
duration: Date.now() - startTime,
|
|
276
|
+
finalFacts,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Find function declaration
|
|
282
|
+
*/
|
|
283
|
+
private findFunction(name: string, inFile: string): ts.FunctionDeclaration | ts.ArrowFunction | null {
|
|
284
|
+
const resolvedPath = path.resolve(inFile);
|
|
285
|
+
|
|
286
|
+
for (const sf of this.sourceFiles) {
|
|
287
|
+
if (!sf.fileName.includes(path.dirname(resolvedPath))) continue;
|
|
288
|
+
|
|
289
|
+
let result: ts.FunctionDeclaration | ts.ArrowFunction | null = null;
|
|
290
|
+
|
|
291
|
+
const visit = (node: ts.Node): void => {
|
|
292
|
+
if (result) return;
|
|
293
|
+
|
|
294
|
+
if (ts.isFunctionDeclaration(node) && node.name?.text === name) {
|
|
295
|
+
result = node;
|
|
296
|
+
} else if (ts.isArrowFunction(node)) {
|
|
297
|
+
const parent = node.parent;
|
|
298
|
+
if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name) && parent.name.text === name) {
|
|
299
|
+
result = node;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
ts.forEachChild(node, visit);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
visit(sf);
|
|
307
|
+
if (result) return result;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Build Control Flow Graph with basic blocks
|
|
315
|
+
*/
|
|
316
|
+
private buildCFG(func: ts.FunctionDeclaration | ts.ArrowFunction): ControlFlowGraph {
|
|
317
|
+
const cacheKey = `${func.getSourceFile().fileName}:${func.getStart()}`;
|
|
318
|
+
if (this.cfgCache.has(cacheKey)) {
|
|
319
|
+
return this.cfgCache.get(cacheKey)!;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const blocks = new Map<string, BasicBlock>();
|
|
323
|
+
let blockId = 0;
|
|
324
|
+
|
|
325
|
+
const createBlock = (): BasicBlock => {
|
|
326
|
+
const id = `block_${blockId++}`;
|
|
327
|
+
const block: BasicBlock = {
|
|
328
|
+
id,
|
|
329
|
+
statements: [],
|
|
330
|
+
predecessors: [],
|
|
331
|
+
successors: [],
|
|
332
|
+
};
|
|
333
|
+
blocks.set(id, block);
|
|
334
|
+
return block;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// Entry block
|
|
338
|
+
const entryBlock = createBlock();
|
|
339
|
+
let currentBlock = entryBlock;
|
|
340
|
+
|
|
341
|
+
// Process function body
|
|
342
|
+
const body = func.body;
|
|
343
|
+
if (!body) {
|
|
344
|
+
const exitBlock = createBlock();
|
|
345
|
+
currentBlock.successors.push(exitBlock.id);
|
|
346
|
+
exitBlock.predecessors.push(currentBlock.id);
|
|
347
|
+
const cfg = { blocks, entryBlock: entryBlock.id, exitBlock: exitBlock.id };
|
|
348
|
+
this.cfgCache.set(cacheKey, cfg);
|
|
349
|
+
return cfg;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Traverse and create blocks
|
|
353
|
+
const processStatement = (stmt: ts.Statement): void => {
|
|
354
|
+
// Handle if statement (creates branches)
|
|
355
|
+
if (ts.isIfStatement(stmt)) {
|
|
356
|
+
// Create branch blocks
|
|
357
|
+
const thenBlock = createBlock();
|
|
358
|
+
const elseBlock = createBlock();
|
|
359
|
+
const mergeBlock = createBlock();
|
|
360
|
+
|
|
361
|
+
// Set up branch info
|
|
362
|
+
currentBlock.branch = {
|
|
363
|
+
condition: stmt.expression,
|
|
364
|
+
trueTarget: thenBlock.id,
|
|
365
|
+
falseTarget: elseBlock.id,
|
|
366
|
+
};
|
|
367
|
+
currentBlock.successors.push(thenBlock.id, elseBlock.id);
|
|
368
|
+
thenBlock.predecessors.push(currentBlock.id);
|
|
369
|
+
elseBlock.predecessors.push(currentBlock.id);
|
|
370
|
+
|
|
371
|
+
// Process then statement
|
|
372
|
+
currentBlock = thenBlock;
|
|
373
|
+
if (ts.isStatement(stmt.thenStatement)) {
|
|
374
|
+
processStatement(stmt.thenStatement);
|
|
375
|
+
} else if (ts.isStatement(stmt.thenStatement)) {
|
|
376
|
+
currentBlock.statements.push(stmt.thenStatement);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Add merge as successor
|
|
380
|
+
if (!currentBlock.successors.includes(mergeBlock.id)) {
|
|
381
|
+
currentBlock.successors.push(mergeBlock.id);
|
|
382
|
+
mergeBlock.predecessors.push(currentBlock.id);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Process else statement
|
|
386
|
+
if (stmt.elseStatement) {
|
|
387
|
+
currentBlock = elseBlock;
|
|
388
|
+
if (ts.isStatement(stmt.elseStatement)) {
|
|
389
|
+
processStatement(stmt.elseStatement);
|
|
390
|
+
} else if (ts.isStatement(stmt.elseStatement)) {
|
|
391
|
+
currentBlock.statements.push(stmt.elseStatement);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (!currentBlock.successors.includes(mergeBlock.id)) {
|
|
395
|
+
currentBlock.successors.push(mergeBlock.id);
|
|
396
|
+
mergeBlock.predecessors.push(currentBlock.id);
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
// No else - add edge from else block to merge
|
|
400
|
+
elseBlock.successors.push(mergeBlock.id);
|
|
401
|
+
mergeBlock.predecessors.push(elseBlock.id);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
currentBlock = mergeBlock;
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Handle while/do-while/for loops
|
|
409
|
+
if (ts.isWhileStatement(stmt) || ts.isForStatement(stmt) || ts.isForInStatement(stmt) || ts.isForOfStatement(stmt)) {
|
|
410
|
+
const loopHeader = createBlock();
|
|
411
|
+
const loopBody = createBlock();
|
|
412
|
+
const loopExit = createBlock();
|
|
413
|
+
|
|
414
|
+
currentBlock.successors.push(loopHeader.id);
|
|
415
|
+
loopHeader.predecessors.push(currentBlock.id);
|
|
416
|
+
|
|
417
|
+
currentBlock = loopHeader;
|
|
418
|
+
let loopCondition: ts.Expression | undefined;
|
|
419
|
+
if (ts.isWhileStatement(stmt)) {
|
|
420
|
+
loopCondition = stmt.expression;
|
|
421
|
+
} else if (ts.isForStatement(stmt)) {
|
|
422
|
+
loopCondition = stmt.condition;
|
|
423
|
+
} else if (ts.isForInStatement(stmt)) {
|
|
424
|
+
loopCondition = stmt.expression;
|
|
425
|
+
} else if (ts.isForOfStatement(stmt)) {
|
|
426
|
+
loopCondition = stmt.expression;
|
|
427
|
+
}
|
|
428
|
+
if (!loopCondition) return;
|
|
429
|
+
currentBlock.branch = {
|
|
430
|
+
condition: loopCondition,
|
|
431
|
+
trueTarget: loopBody.id,
|
|
432
|
+
falseTarget: loopExit.id,
|
|
433
|
+
};
|
|
434
|
+
currentBlock.successors.push(loopBody.id, loopExit.id);
|
|
435
|
+
loopBody.predecessors.push(currentBlock.id);
|
|
436
|
+
|
|
437
|
+
currentBlock = loopBody;
|
|
438
|
+
processStatement(stmt.statement);
|
|
439
|
+
currentBlock.successors.push(loopHeader.id);
|
|
440
|
+
loopHeader.predecessors.push(currentBlock.id);
|
|
441
|
+
|
|
442
|
+
currentBlock = loopExit;
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Handle try-catch-finally
|
|
447
|
+
if (ts.isTryStatement(stmt)) {
|
|
448
|
+
const tryBlock = createBlock();
|
|
449
|
+
const catchBlock = createBlock();
|
|
450
|
+
const finallyBlock = createBlock();
|
|
451
|
+
|
|
452
|
+
currentBlock.successors.push(tryBlock.id);
|
|
453
|
+
tryBlock.predecessors.push(currentBlock.id);
|
|
454
|
+
|
|
455
|
+
currentBlock = tryBlock;
|
|
456
|
+
processStatement(stmt.tryBlock);
|
|
457
|
+
|
|
458
|
+
if (stmt.catchClause) {
|
|
459
|
+
currentBlock.successors.push(catchBlock.id);
|
|
460
|
+
catchBlock.predecessors.push(currentBlock.id);
|
|
461
|
+
currentBlock = catchBlock;
|
|
462
|
+
processStatement(stmt.catchClause.block);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
currentBlock.successors.push(finallyBlock.id);
|
|
466
|
+
finallyBlock.predecessors.push(currentBlock.id);
|
|
467
|
+
finallyBlock.successors.push(finallyBlock.id); // exits to itself then to next
|
|
468
|
+
currentBlock = finallyBlock;
|
|
469
|
+
if (stmt.finallyBlock) {
|
|
470
|
+
processStatement(stmt.finallyBlock);
|
|
471
|
+
}
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Handle return statement
|
|
476
|
+
if (ts.isReturnStatement(stmt)) {
|
|
477
|
+
currentBlock.statements.push(stmt);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Handle switch statement
|
|
482
|
+
if (ts.isSwitchStatement(stmt)) {
|
|
483
|
+
const mergeBlock = createBlock();
|
|
484
|
+
const caseBlocks: BasicBlock[] = [];
|
|
485
|
+
|
|
486
|
+
for (const clause of stmt.caseBlock.clauses) {
|
|
487
|
+
const caseBlock = createBlock();
|
|
488
|
+
caseBlocks.push(caseBlock);
|
|
489
|
+
currentBlock.successors.push(caseBlock.id);
|
|
490
|
+
caseBlock.predecessors.push(currentBlock.id);
|
|
491
|
+
currentBlock = caseBlock;
|
|
492
|
+
|
|
493
|
+
for (const s of clause.statements) {
|
|
494
|
+
processStatement(s);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (!currentBlock.successors.includes(mergeBlock.id)) {
|
|
498
|
+
currentBlock.successors.push(mergeBlock.id);
|
|
499
|
+
mergeBlock.predecessors.push(currentBlock.id);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
currentBlock = mergeBlock;
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Regular statement - add to current block
|
|
508
|
+
currentBlock.statements.push(stmt);
|
|
509
|
+
|
|
510
|
+
// Check for control flow statements that might branch
|
|
511
|
+
if (ts.isBreakStatement(stmt) || ts.isContinueStatement(stmt) || ts.isThrowStatement(stmt)) {
|
|
512
|
+
// These will be handled when we add proper CFG edges
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// Process body statements
|
|
517
|
+
if (ts.isBlock(body)) {
|
|
518
|
+
for (const stmt of body.statements) {
|
|
519
|
+
processStatement(stmt);
|
|
520
|
+
}
|
|
521
|
+
} else {
|
|
522
|
+
// Expression body (arrow function)
|
|
523
|
+
currentBlock.statements.push(body as any);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Create exit block
|
|
527
|
+
const exitBlock = createBlock();
|
|
528
|
+
currentBlock.successors.push(exitBlock.id);
|
|
529
|
+
exitBlock.predecessors.push(currentBlock.id);
|
|
530
|
+
|
|
531
|
+
const cfg = { blocks, entryBlock: entryBlock.id, exitBlock: exitBlock.id };
|
|
532
|
+
this.cfgCache.set(cacheKey, cfg);
|
|
533
|
+
return cfg;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Create entry fact with parameter bindings
|
|
538
|
+
*/
|
|
539
|
+
private createEntryFact(func: ts.FunctionDeclaration | ts.ArrowFunction): DataFlowFact {
|
|
540
|
+
const env = new Map<string, AbstractValue>();
|
|
541
|
+
|
|
542
|
+
// Add parameters
|
|
543
|
+
for (const param of func.parameters) {
|
|
544
|
+
if (ts.isIdentifier(param.name)) {
|
|
545
|
+
const paramType = this.checker.getTypeAtLocation(param);
|
|
546
|
+
env.set(param.name.text, this.createAbstractValue(paramType));
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return {
|
|
551
|
+
env,
|
|
552
|
+
constraints: [],
|
|
553
|
+
pathCondition: [],
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Create abstract value from TypeScript type
|
|
559
|
+
*/
|
|
560
|
+
private createAbstractValue(type: ts.Type): AbstractValue {
|
|
561
|
+
const typeStr = this.checker.typeToString(type);
|
|
562
|
+
const flags = type.flags;
|
|
563
|
+
|
|
564
|
+
const value: AbstractValue = {
|
|
565
|
+
type: typeStr,
|
|
566
|
+
constants: new Set(),
|
|
567
|
+
nullable: false,
|
|
568
|
+
properties: new Map(),
|
|
569
|
+
tainted: false,
|
|
570
|
+
escapes: new Set(),
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
// Check for null/undefined
|
|
574
|
+
if (flags & ts.TypeFlags.Null) value.nullable = true;
|
|
575
|
+
if (flags & ts.TypeFlags.Undefined) value.nullable = true;
|
|
576
|
+
if (flags & ts.TypeFlags.StringLiteral) {
|
|
577
|
+
value.constants.add((type as any).value || typeStr);
|
|
578
|
+
}
|
|
579
|
+
if (flags & ts.TypeFlags.NumberLiteral) {
|
|
580
|
+
value.constants.add(String((type as any).value || typeStr));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Handle object types
|
|
584
|
+
if (flags & ts.TypeFlags.Object) {
|
|
585
|
+
const objType = type as ts.ObjectType;
|
|
586
|
+
const props = this.checker.getPropertiesOfType(objType);
|
|
587
|
+
for (const prop of props) {
|
|
588
|
+
if (prop.valueDeclaration) {
|
|
589
|
+
const propType = this.checker.getTypeAtLocation(prop.valueDeclaration);
|
|
590
|
+
value.properties.set(prop.name, this.createAbstractValue(propType));
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Handle type references (interfaces, classes) - check for typeArguments
|
|
596
|
+
const typeRef = type as ts.TypeReference;
|
|
597
|
+
if (typeRef.typeArguments && typeRef.typeArguments.length > 0) {
|
|
598
|
+
// Generic type reference
|
|
599
|
+
if (typeRef.target) {
|
|
600
|
+
value.type = this.checker.typeToString(typeRef.target);
|
|
601
|
+
}
|
|
602
|
+
value.elementType = this.createAbstractValue(typeRef.typeArguments[0]);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return value;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* WORKLIST ALGORITHM - Lattice-based fixed-point computation
|
|
610
|
+
*
|
|
611
|
+
* This is the core of the data flow analysis.
|
|
612
|
+
* It iterates until no facts change (fixed point is reached).
|
|
613
|
+
*/
|
|
614
|
+
private runWorklistAnalysis(
|
|
615
|
+
cfg: ControlFlowGraph,
|
|
616
|
+
entryFact: DataFlowFact,
|
|
617
|
+
stats: DataFlowResult['statistics']
|
|
618
|
+
): Map<string, DataFlowFact> {
|
|
619
|
+
const blockFacts = new Map<string, DataFlowFact>();
|
|
620
|
+
const changed = new Set<string>();
|
|
621
|
+
|
|
622
|
+
// Initialize all blocks with BOTTOM (no information)
|
|
623
|
+
for (const blockId of cfg.blocks.keys()) {
|
|
624
|
+
blockFacts.set(blockId, {
|
|
625
|
+
env: new Map(),
|
|
626
|
+
constraints: [],
|
|
627
|
+
pathCondition: [],
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Set entry block
|
|
632
|
+
blockFacts.set(cfg.entryBlock, this.cloneFact(entryFact));
|
|
633
|
+
this.worklist.push(cfg.entryBlock);
|
|
634
|
+
changed.add(cfg.entryBlock);
|
|
635
|
+
|
|
636
|
+
let iterations = 0;
|
|
637
|
+
|
|
638
|
+
// Worklist algorithm
|
|
639
|
+
while (this.worklist.length > 0 && iterations < this.maxIterations) {
|
|
640
|
+
iterations++;
|
|
641
|
+
stats.fixedPointIterations++;
|
|
642
|
+
|
|
643
|
+
// Pop from worklist
|
|
644
|
+
const blockId = this.worklist.shift()!;
|
|
645
|
+
changed.delete(blockId);
|
|
646
|
+
|
|
647
|
+
const block = cfg.blocks.get(blockId)!;
|
|
648
|
+
const currentFact = blockFacts.get(blockId)!;
|
|
649
|
+
|
|
650
|
+
// Compute flow through predecessors (JOIN)
|
|
651
|
+
if (block.predecessors.length > 0) {
|
|
652
|
+
const joinedFact = this.joinFacts(
|
|
653
|
+
block.predecessors.map(predId => blockFacts.get(predId)!)
|
|
654
|
+
);
|
|
655
|
+
// If join changed anything, update and propagate
|
|
656
|
+
if (!this.factsEqual(currentFact, joinedFact)) {
|
|
657
|
+
blockFacts.set(blockId, joinedFact);
|
|
658
|
+
for (const succId of block.successors) {
|
|
659
|
+
if (!changed.has(succId)) {
|
|
660
|
+
this.worklist.push(succId);
|
|
661
|
+
changed.add(succId);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// TRANSFER FUNCTION - apply block's statements
|
|
669
|
+
let newFact = this.cloneFact(currentFact);
|
|
670
|
+
|
|
671
|
+
for (const stmt of block.statements) {
|
|
672
|
+
newFact = this.transfer(stmt, newFact, stats);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Handle branch condition (add to path condition)
|
|
676
|
+
if (block.branch) {
|
|
677
|
+
stats.conditionalBranches++;
|
|
678
|
+
const thenFact = this.cloneFact(newFact);
|
|
679
|
+
const elseFact = this.cloneFact(newFact);
|
|
680
|
+
|
|
681
|
+
// Add path condition for then-branch and NARROW TYPES
|
|
682
|
+
thenFact.pathCondition.push({
|
|
683
|
+
expression: block.branch.condition.getText(),
|
|
684
|
+
polarity: true,
|
|
685
|
+
});
|
|
686
|
+
// Apply type narrowing based on condition
|
|
687
|
+
this.narrowTypesFromCondition(thenFact, block.branch.condition, true, stats);
|
|
688
|
+
|
|
689
|
+
// Add path condition for else-branch (negated) and NARROW TYPES
|
|
690
|
+
elseFact.pathCondition.push({
|
|
691
|
+
expression: block.branch.condition.getText(),
|
|
692
|
+
polarity: false,
|
|
693
|
+
});
|
|
694
|
+
// Apply type narrowing based on negated condition
|
|
695
|
+
this.narrowTypesFromCondition(elseFact, block.branch.condition, false, stats);
|
|
696
|
+
|
|
697
|
+
// Propagate to successors
|
|
698
|
+
const thenBlock = cfg.blocks.get(block.branch.trueTarget)!;
|
|
699
|
+
const elseBlock = cfg.blocks.get(block.branch.falseTarget)!;
|
|
700
|
+
|
|
701
|
+
if (!this.factsEqual(blockFacts.get(thenBlock.id)!, thenFact)) {
|
|
702
|
+
blockFacts.set(thenBlock.id, thenFact);
|
|
703
|
+
if (!changed.has(thenBlock.id)) {
|
|
704
|
+
this.worklist.push(thenBlock.id);
|
|
705
|
+
changed.add(thenBlock.id);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (!this.factsEqual(blockFacts.get(elseBlock.id)!, elseFact)) {
|
|
710
|
+
blockFacts.set(elseBlock.id, elseFact);
|
|
711
|
+
if (!changed.has(elseBlock.id)) {
|
|
712
|
+
this.worklist.push(elseBlock.id);
|
|
713
|
+
changed.add(elseBlock.id);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
} else {
|
|
717
|
+
// No branch - normal flow to successors
|
|
718
|
+
for (const succId of block.successors) {
|
|
719
|
+
if (!this.factsEqual(blockFacts.get(succId)!, newFact)) {
|
|
720
|
+
blockFacts.set(succId, newFact);
|
|
721
|
+
if (!changed.has(succId)) {
|
|
722
|
+
this.worklist.push(succId);
|
|
723
|
+
changed.add(succId);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
stats.nodesAnalyzed++;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return blockFacts;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* JOIN operation - combine facts from multiple predecessors
|
|
737
|
+
*/
|
|
738
|
+
private joinFacts(facts: DataFlowFact[]): DataFlowFact {
|
|
739
|
+
if (facts.length === 0) {
|
|
740
|
+
return {
|
|
741
|
+
env: new Map(),
|
|
742
|
+
constraints: [],
|
|
743
|
+
pathCondition: [],
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (facts.length === 1) {
|
|
748
|
+
return this.cloneFact(facts[0]);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const result: DataFlowFact = {
|
|
752
|
+
env: new Map(),
|
|
753
|
+
constraints: [],
|
|
754
|
+
pathCondition: [],
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
// Join all environments
|
|
758
|
+
const allVars = new Set<string>();
|
|
759
|
+
for (const fact of facts) {
|
|
760
|
+
for (const [v] of fact.env) {
|
|
761
|
+
allVars.add(v);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
for (const varName of allVars) {
|
|
766
|
+
const values = facts
|
|
767
|
+
.map(f => f.env.get(varName))
|
|
768
|
+
.filter((v): v is AbstractValue => v !== undefined);
|
|
769
|
+
|
|
770
|
+
if (values.length === 0) continue;
|
|
771
|
+
|
|
772
|
+
// Lattice meet operation (intersection of possible values)
|
|
773
|
+
result.env.set(varName, this.latticeMeet(values));
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Union path conditions
|
|
777
|
+
for (const fact of facts) {
|
|
778
|
+
for (const pc of fact.pathCondition) {
|
|
779
|
+
if (!result.pathCondition.some(p => p.expression === pc.expression && p.polarity === pc.polarity)) {
|
|
780
|
+
result.pathCondition.push(pc);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return result;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* LATTICE MEET - intersection of abstract values
|
|
790
|
+
*/
|
|
791
|
+
private latticeMeet(values: AbstractValue[]): AbstractValue {
|
|
792
|
+
if (values.length === 0) {
|
|
793
|
+
return this.createAbstractValue(this.checker.getTypeAtLocation(ts.factory.createIdentifier('undefined')));
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (values.length === 1) {
|
|
797
|
+
return values[0];
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// For now, simplified meet: union of constants, intersection of types
|
|
801
|
+
const result: AbstractValue = {
|
|
802
|
+
type: values[0].type,
|
|
803
|
+
constants: new Set(),
|
|
804
|
+
nullable: values.some(v => v.nullable),
|
|
805
|
+
properties: new Map(),
|
|
806
|
+
tainted: values.some(v => v.tainted),
|
|
807
|
+
escapes: new Set(),
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
// Union of constants
|
|
811
|
+
for (const v of values) {
|
|
812
|
+
for (const c of v.constants) {
|
|
813
|
+
result.constants.add(c);
|
|
814
|
+
}
|
|
815
|
+
for (const e of v.escapes) {
|
|
816
|
+
result.escapes.add(e);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Intersect property types (simplified)
|
|
821
|
+
const allProps = new Set<string>();
|
|
822
|
+
for (const v of values) {
|
|
823
|
+
for (const [p] of v.properties) {
|
|
824
|
+
allProps.add(p);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
for (const propName of allProps) {
|
|
829
|
+
const propValues = values
|
|
830
|
+
.map(v => v.properties.get(propName))
|
|
831
|
+
.filter((v): v is AbstractValue => v !== undefined);
|
|
832
|
+
|
|
833
|
+
if (propValues.length > 0) {
|
|
834
|
+
result.properties.set(propName, this.latticeMeet(propValues));
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return result;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* TRANSFER FUNCTION - apply a statement's effect on facts
|
|
843
|
+
*/
|
|
844
|
+
private transfer(stmt: ts.Statement, fact: DataFlowFact, stats: DataFlowResult['statistics']): DataFlowFact {
|
|
845
|
+
// Variable declaration
|
|
846
|
+
if (ts.isVariableStatement(stmt)) {
|
|
847
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
848
|
+
if (ts.isIdentifier(decl.name) && decl.initializer) {
|
|
849
|
+
const varName = decl.name.text;
|
|
850
|
+
const rhsFact = this.transferExpr(decl.initializer, fact, stats);
|
|
851
|
+
const rhsValue = this.evaluateExpr(decl.initializer, rhsFact);
|
|
852
|
+
|
|
853
|
+
// Check for taint (user input)
|
|
854
|
+
if (this.isTaintedSource(decl.initializer)) {
|
|
855
|
+
rhsValue.tainted = true;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Check for escape
|
|
859
|
+
if (this.doesEscape(decl.initializer)) {
|
|
860
|
+
rhsValue.escapes.add('return');
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
fact.env.set(varName, rhsValue);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return fact;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Assignment
|
|
870
|
+
if (ts.isExpressionStatement(stmt) && ts.isBinaryExpression(stmt.expression) && stmt.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
871
|
+
const assign = stmt.expression;
|
|
872
|
+
if (ts.isIdentifier(assign.left)) {
|
|
873
|
+
const varName = assign.left.text;
|
|
874
|
+
const rhsFact = this.transferExpr(assign.right, fact, stats);
|
|
875
|
+
const rhsValue = this.evaluateExpr(assign.right, rhsFact);
|
|
876
|
+
|
|
877
|
+
// Check for taint propagation
|
|
878
|
+
const rhsFactValue = this.evaluateExpr(assign.right, fact);
|
|
879
|
+
if (rhsFactValue.tainted) {
|
|
880
|
+
rhsValue.tainted = true;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
fact.env.set(varName, rhsValue);
|
|
884
|
+
}
|
|
885
|
+
return fact;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Return statement
|
|
889
|
+
if (ts.isReturnStatement(stmt) && stmt.expression) {
|
|
890
|
+
const retValue = this.evaluateExpr(stmt.expression, fact);
|
|
891
|
+
// Mark as escaping via return
|
|
892
|
+
retValue.escapes.add('return');
|
|
893
|
+
fact.env.set('__return__', retValue);
|
|
894
|
+
return fact;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Call expression
|
|
898
|
+
if (ts.isExpressionStatement(stmt) && ts.isCallExpression(stmt.expression)) {
|
|
899
|
+
this.analyzeCallSite(stmt.expression, fact, stats);
|
|
900
|
+
return fact;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
return fact;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Transfer function for expressions
|
|
908
|
+
*/
|
|
909
|
+
private transferExpr(expr: ts.Expression, fact: DataFlowFact, stats: DataFlowResult['statistics']): DataFlowFact {
|
|
910
|
+
// Handle conditional expression (ternary)
|
|
911
|
+
if (ts.isConditionalExpression(expr)) {
|
|
912
|
+
// Both branches contribute
|
|
913
|
+
const thenFact = this.transferExpr(expr.whenTrue, fact, stats);
|
|
914
|
+
const elseFact = this.transferExpr(expr.whenFalse, fact, stats);
|
|
915
|
+
return this.joinFacts([thenFact, elseFact]);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return fact;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Evaluate expression to get abstract value
|
|
923
|
+
*/
|
|
924
|
+
private evaluateExpr(expr: ts.Expression, fact: DataFlowFact): AbstractValue {
|
|
925
|
+
// Identifier
|
|
926
|
+
if (ts.isIdentifier(expr)) {
|
|
927
|
+
const varName = expr.text;
|
|
928
|
+
const value = fact.env.get(varName);
|
|
929
|
+
if (value) return value;
|
|
930
|
+
// Unknown variable
|
|
931
|
+
const unknownType = this.checker.getTypeAtLocation(expr);
|
|
932
|
+
return this.createAbstractValue(unknownType);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// String literal
|
|
936
|
+
if (ts.isStringLiteral(expr)) {
|
|
937
|
+
return {
|
|
938
|
+
type: 'string',
|
|
939
|
+
constants: new Set([`'${expr.text}'`]),
|
|
940
|
+
nullable: false,
|
|
941
|
+
properties: new Map(),
|
|
942
|
+
tainted: false,
|
|
943
|
+
escapes: new Set(),
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Numeric literal
|
|
948
|
+
if (ts.isNumericLiteral(expr)) {
|
|
949
|
+
return {
|
|
950
|
+
type: 'number',
|
|
951
|
+
constants: new Set([expr.text]),
|
|
952
|
+
nullable: false,
|
|
953
|
+
properties: new Map(),
|
|
954
|
+
tainted: false,
|
|
955
|
+
escapes: new Set(),
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Property access (obj.prop)
|
|
960
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
961
|
+
const objValue = this.evaluateExpr(expr.expression, fact);
|
|
962
|
+
const propName = expr.name.text;
|
|
963
|
+
|
|
964
|
+
// Look up property in object's type
|
|
965
|
+
const propValue = objValue.properties.get(propName);
|
|
966
|
+
if (propValue) return propValue;
|
|
967
|
+
|
|
968
|
+
// Or use the type checker
|
|
969
|
+
const type = this.checker.getTypeAtLocation(expr);
|
|
970
|
+
return this.createAbstractValue(type);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Call expression
|
|
974
|
+
if (ts.isCallExpression(expr)) {
|
|
975
|
+
const type = this.checker.getTypeAtLocation(expr);
|
|
976
|
+
return this.createAbstractValue(type);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Binary expression
|
|
980
|
+
if (ts.isBinaryExpression(expr)) {
|
|
981
|
+
const type = this.checker.getTypeAtLocation(expr);
|
|
982
|
+
return this.createAbstractValue(type);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// Element access (arr[i])
|
|
986
|
+
if (ts.isElementAccessExpression(expr)) {
|
|
987
|
+
const type = this.checker.getTypeAtLocation(expr);
|
|
988
|
+
return this.createAbstractValue(type);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// Default - use type checker
|
|
992
|
+
const type = this.checker.getTypeAtLocation(expr);
|
|
993
|
+
return this.createAbstractValue(type);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* Analyze a call site (interprocedural analysis)
|
|
998
|
+
*/
|
|
999
|
+
private analyzeCallSite(callExpr: ts.CallExpression, fact: DataFlowFact, stats: DataFlowResult['statistics']): void {
|
|
1000
|
+
const calleeExpr = callExpr.expression;
|
|
1001
|
+
let calleeName = '';
|
|
1002
|
+
|
|
1003
|
+
if (ts.isIdentifier(calleeExpr)) {
|
|
1004
|
+
calleeName = calleeExpr.text;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Check for known taint sources
|
|
1008
|
+
if (calleeName === 'readFile' || calleeName === 'readFileSync') {
|
|
1009
|
+
stats.taintedValues++;
|
|
1010
|
+
}
|
|
1011
|
+
if (calleeName === 'fetch' || calleeName === 'axios' || calleeName === 'http.get') {
|
|
1012
|
+
// Network input is potentially tainted
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Try to resolve the callee function
|
|
1016
|
+
const type = this.checker.getTypeAtLocation(calleeExpr);
|
|
1017
|
+
const symbol = type.symbol;
|
|
1018
|
+
if (!symbol) return;
|
|
1019
|
+
|
|
1020
|
+
const declarations = symbol.getDeclarations();
|
|
1021
|
+
if (!declarations || declarations.length === 0) return;
|
|
1022
|
+
|
|
1023
|
+
const calleeFunc = declarations[0];
|
|
1024
|
+
if (!ts.isFunctionDeclaration(calleeFunc) && !ts.isArrowFunction(calleeFunc)) return;
|
|
1025
|
+
|
|
1026
|
+
stats.callSitesAnalyzed++;
|
|
1027
|
+
|
|
1028
|
+
// Build argument facts
|
|
1029
|
+
const argFacts = new Map<string, AbstractValue>();
|
|
1030
|
+
for (let i = 0; i < callExpr.arguments.length; i++) {
|
|
1031
|
+
const arg = callExpr.arguments[i];
|
|
1032
|
+
const paramName = calleeFunc.parameters[i]?.name;
|
|
1033
|
+
if (ts.isIdentifier(paramName) && paramName.text) {
|
|
1034
|
+
argFacts.set(paramName.text, this.evaluateExpr(arg, fact));
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Check if we've analyzed this call site (context sensitivity cache)
|
|
1039
|
+
const callKey = `${callExpr.getSourceFile().fileName}:${callExpr.getStart()}:${calleeName}`;
|
|
1040
|
+
if (this.analyzedCallSites.has(callKey)) return;
|
|
1041
|
+
this.analyzedCallSites.add(callKey);
|
|
1042
|
+
|
|
1043
|
+
// Build CFG for callee and analyze with argument bindings
|
|
1044
|
+
const calleeCFG = this.buildCFG(calleeFunc);
|
|
1045
|
+
const calleeEntryFact = this.createEntryFact(calleeFunc);
|
|
1046
|
+
|
|
1047
|
+
// Override with actual argument values
|
|
1048
|
+
for (const [paramName, argValue] of argFacts) {
|
|
1049
|
+
calleeEntryFact.env.set(paramName, argValue);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Run analysis on callee
|
|
1053
|
+
this.runWorklistAnalysis(calleeCFG, calleeEntryFact, stats);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* Check if expression is a taint source
|
|
1058
|
+
*/
|
|
1059
|
+
private isTaintedSource(expr: ts.Expression): boolean {
|
|
1060
|
+
if (ts.isCallExpression(expr)) {
|
|
1061
|
+
const callee = expr.expression;
|
|
1062
|
+
if (ts.isIdentifier(callee)) {
|
|
1063
|
+
const name = callee.text;
|
|
1064
|
+
// Known taint sources
|
|
1065
|
+
if (['readFile', 'readFileSync', 'fetch', 'axios', 'http.request',
|
|
1066
|
+
'process.argv', 'process.env', 'JSON.parse', 'document.cookie',
|
|
1067
|
+
'localStorage.getItem', 'sessionStorage.getItem'].includes(name)) {
|
|
1068
|
+
return true;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
return false;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* Check if expression causes escape
|
|
1077
|
+
*/
|
|
1078
|
+
private doesEscape(expr: ts.Expression): boolean {
|
|
1079
|
+
// Return statement makes value escape
|
|
1080
|
+
if (ts.isReturnStatement(expr)) return true;
|
|
1081
|
+
|
|
1082
|
+
// Passing as argument makes it escape to that function
|
|
1083
|
+
if (ts.isCallExpression(expr)) return true;
|
|
1084
|
+
|
|
1085
|
+
// Property assignment to external object
|
|
1086
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
1087
|
+
const propAccess = expr;
|
|
1088
|
+
if (ts.isIdentifier(propAccess.expression)) {
|
|
1089
|
+
const name = propAccess.expression.text;
|
|
1090
|
+
// Known external objects
|
|
1091
|
+
if (['global', 'window', 'document', 'console', 'process'].includes(name)) {
|
|
1092
|
+
return true;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
return false;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Analyze async patterns (Promise, await)
|
|
1102
|
+
*/
|
|
1103
|
+
private analyzeAsyncPatterns(
|
|
1104
|
+
func: ts.FunctionDeclaration | ts.ArrowFunction,
|
|
1105
|
+
flowPaths: FlowPath[],
|
|
1106
|
+
stats: DataFlowResult['statistics']
|
|
1107
|
+
): void {
|
|
1108
|
+
const visit = (node: ts.Node): void => {
|
|
1109
|
+
// Promise.then chain
|
|
1110
|
+
if (ts.isPropertyAccessExpression(node) && node.name.text === 'then') {
|
|
1111
|
+
const callExpr = node.parent;
|
|
1112
|
+
if (ts.isCallExpression(callExpr)) {
|
|
1113
|
+
const callback = callExpr.arguments[0];
|
|
1114
|
+
if (ts.isArrowFunction(callback) && callback.parameters.length > 0) {
|
|
1115
|
+
stats.promiseUnwraps++;
|
|
1116
|
+
|
|
1117
|
+
const paramType = this.checker.getTypeAtLocation(callback.parameters[0]);
|
|
1118
|
+
const paramTypeStr = this.checker.typeToString(paramType);
|
|
1119
|
+
|
|
1120
|
+
flowPaths.push({
|
|
1121
|
+
source: `Promise.then callback at line ${this.getLine(node.getSourceFile(), node)}`,
|
|
1122
|
+
sink: `${paramTypeStr} at line ${this.getLine(node.getSourceFile(), callback)}`,
|
|
1123
|
+
path: ['Promise', 'then', 'callback'],
|
|
1124
|
+
typeAtSink: paramTypeStr,
|
|
1125
|
+
typeAtSource: 'T (Promise<T>)',
|
|
1126
|
+
isTainted: false,
|
|
1127
|
+
});
|
|
1128
|
+
stats.pathsTracked++;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// await expression
|
|
1134
|
+
if (ts.isAwaitExpression(node)) {
|
|
1135
|
+
stats.promiseUnwraps++;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
ts.forEachChild(node, visit);
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
visit(func);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* Check for data leaks (tainted -> escape)
|
|
1146
|
+
*/
|
|
1147
|
+
private checkDataLeaks(flowPaths: FlowPath[], taintedPaths: TaintedPath[]): boolean {
|
|
1148
|
+
// If any tainted path reaches a sensitive sink, it's a leak
|
|
1149
|
+
for (const tp of taintedPaths) {
|
|
1150
|
+
if (tp.sink.includes('return') || tp.sink.includes('global') || tp.sink.includes('write')) {
|
|
1151
|
+
return true;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
return false;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* Clone a data flow fact
|
|
1159
|
+
*/
|
|
1160
|
+
private cloneFact(fact: DataFlowFact): DataFlowFact {
|
|
1161
|
+
const cloned = {
|
|
1162
|
+
env: new Map<string, AbstractValue>(),
|
|
1163
|
+
constraints: [...fact.constraints],
|
|
1164
|
+
pathCondition: [...fact.pathCondition],
|
|
1165
|
+
};
|
|
1166
|
+
|
|
1167
|
+
for (const [key, value] of fact.env) {
|
|
1168
|
+
cloned.env.set(key, {
|
|
1169
|
+
...value,
|
|
1170
|
+
constants: new Set(value.constants),
|
|
1171
|
+
properties: new Map(value.properties),
|
|
1172
|
+
escapes: new Set(value.escapes),
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
return cloned;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Narrow types based on branch condition
|
|
1181
|
+
* E.g., if (x != null) narrows x from T | null to T
|
|
1182
|
+
* if (x > 1000) narrows the possible range of x
|
|
1183
|
+
*/
|
|
1184
|
+
private narrowTypesFromCondition(
|
|
1185
|
+
fact: DataFlowFact,
|
|
1186
|
+
cond: ts.Expression,
|
|
1187
|
+
isThenBranch: boolean,
|
|
1188
|
+
stats: DataFlowResult['statistics']
|
|
1189
|
+
): void {
|
|
1190
|
+
// Handle binary expressions: x != null, x === 'value', x > 1000, etc.
|
|
1191
|
+
if (ts.isBinaryExpression(cond)) {
|
|
1192
|
+
const left = cond.left;
|
|
1193
|
+
const right = cond.right;
|
|
1194
|
+
const op = cond.operatorToken.kind;
|
|
1195
|
+
|
|
1196
|
+
if (ts.isIdentifier(left)) {
|
|
1197
|
+
const varName = left.text;
|
|
1198
|
+
const varValue = fact.env.get(varName);
|
|
1199
|
+
|
|
1200
|
+
if (!varValue) return;
|
|
1201
|
+
|
|
1202
|
+
// Handle null checks: x != null, x !== null, x == null, x === null
|
|
1203
|
+
if (right.kind === ts.SyntaxKind.NullKeyword) {
|
|
1204
|
+
if (op === ts.SyntaxKind.ExclamationEqualsEqualsToken ||
|
|
1205
|
+
op === ts.SyntaxKind.ExclamationEqualsToken) {
|
|
1206
|
+
// Then branch: x != null means x is not null
|
|
1207
|
+
if (isThenBranch) {
|
|
1208
|
+
varValue.nullable = false;
|
|
1209
|
+
stats.typesNarrowed++;
|
|
1210
|
+
}
|
|
1211
|
+
} else if (op === ts.SyntaxKind.EqualsEqualsToken ||
|
|
1212
|
+
op === ts.SyntaxKind.EqualsEqualsEqualsToken) {
|
|
1213
|
+
// Then branch: x == null means x IS null
|
|
1214
|
+
if (isThenBranch) {
|
|
1215
|
+
varValue.constants.add('null');
|
|
1216
|
+
stats.typesNarrowed++;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Handle numeric comparisons: x > 1000, x <= 0
|
|
1222
|
+
if (varValue.type === 'number' && ts.isNumericLiteral(right)) {
|
|
1223
|
+
const numValue = right.text;
|
|
1224
|
+
if (op === ts.SyntaxKind.GreaterThanToken) {
|
|
1225
|
+
if (isThenBranch) {
|
|
1226
|
+
varValue.constants.add(`>${numValue}`);
|
|
1227
|
+
stats.typesNarrowed++;
|
|
1228
|
+
}
|
|
1229
|
+
} else if (op === ts.SyntaxKind.LessThanToken) {
|
|
1230
|
+
if (isThenBranch) {
|
|
1231
|
+
varValue.constants.add(`<${numValue}`);
|
|
1232
|
+
stats.typesNarrowed++;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// Handle instanceof checks
|
|
1239
|
+
if (ts.isBinaryExpression(cond) && ts.isIdentifier(right)) {
|
|
1240
|
+
const rightName = right.text;
|
|
1241
|
+
if (op === ts.SyntaxKind.InstanceOfKeyword) {
|
|
1242
|
+
if (isThenBranch) {
|
|
1243
|
+
const varName = (cond.left as ts.Identifier).text;
|
|
1244
|
+
const varValue = fact.env.get(varName);
|
|
1245
|
+
if (varValue) {
|
|
1246
|
+
varValue.type = rightName;
|
|
1247
|
+
stats.typesNarrowed++;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// Handle unary expressions: if (x)
|
|
1255
|
+
if (ts.isPrefixUnaryExpression(cond) && cond.operator === ts.SyntaxKind.ExclamationToken) {
|
|
1256
|
+
const operand = cond.operand;
|
|
1257
|
+
if (ts.isIdentifier(operand)) {
|
|
1258
|
+
const varName = operand.text;
|
|
1259
|
+
const varValue = fact.env.get(varName);
|
|
1260
|
+
if (varValue) {
|
|
1261
|
+
if (isThenBranch) {
|
|
1262
|
+
// !x means x is falsy (null, undefined, 0, false, '')
|
|
1263
|
+
// We can narrow nullable types
|
|
1264
|
+
if (varValue.nullable) {
|
|
1265
|
+
varValue.nullable = false;
|
|
1266
|
+
stats.typesNarrowed++;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// Handle identifier directly: if (data)
|
|
1274
|
+
if (ts.isIdentifier(cond)) {
|
|
1275
|
+
const varName = cond.text;
|
|
1276
|
+
const varValue = fact.env.get(varName);
|
|
1277
|
+
if (varValue) {
|
|
1278
|
+
if (isThenBranch) {
|
|
1279
|
+
// truthy check - we know it's not null/undefined/false
|
|
1280
|
+
varValue.nullable = false;
|
|
1281
|
+
stats.typesNarrowed++;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* Check if two facts are equal
|
|
1289
|
+
*/
|
|
1290
|
+
private factsEqual(a: DataFlowFact, b: DataFlowFact): boolean {
|
|
1291
|
+
if (a.env.size !== b.env.size) return false;
|
|
1292
|
+
|
|
1293
|
+
for (const [key, aVal] of a.env) {
|
|
1294
|
+
const bVal = b.env.get(key);
|
|
1295
|
+
if (!bVal) return false;
|
|
1296
|
+
if (aVal.type !== bVal.type) return false;
|
|
1297
|
+
if (aVal.tainted !== bVal.tainted) return false;
|
|
1298
|
+
if (aVal.nullable !== bVal.nullable) return false;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
return true;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* Get line number
|
|
1306
|
+
*/
|
|
1307
|
+
private getLine(sourceFile: ts.SourceFile | undefined, node: ts.Node): number {
|
|
1308
|
+
if (!sourceFile) return 0;
|
|
1309
|
+
return sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* Calculate confidence level
|
|
1314
|
+
*/
|
|
1315
|
+
private calculateConfidence(stats: DataFlowResult['statistics']): 'high' | 'medium' | 'low' {
|
|
1316
|
+
if (stats.fixedPointIterations >= 50 && stats.callSitesAnalyzed >= 10) return 'high';
|
|
1317
|
+
if (stats.fixedPointIterations >= 20 && stats.callSitesAnalyzed >= 5) return 'medium';
|
|
1318
|
+
return 'low';
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* Create empty result
|
|
1323
|
+
*/
|
|
1324
|
+
private createEmptyResult(confidence: 'high' | 'medium' | 'low', stats: DataFlowResult['statistics'], duration: number): DataFlowResult {
|
|
1325
|
+
return {
|
|
1326
|
+
hasDataLeaks: false,
|
|
1327
|
+
flowPaths: [],
|
|
1328
|
+
taintedPaths: [],
|
|
1329
|
+
typeNarrowing: new Map(),
|
|
1330
|
+
statistics: stats,
|
|
1331
|
+
confidence,
|
|
1332
|
+
duration,
|
|
1333
|
+
finalFacts: new Map(),
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* Format as text
|
|
1339
|
+
*/
|
|
1340
|
+
formatAsText(result: DataFlowResult, functionName: string): string {
|
|
1341
|
+
const lines: string[] = [];
|
|
1342
|
+
|
|
1343
|
+
lines.push('');
|
|
1344
|
+
lines.push('═══════════════════════════════════════════════════════════════');
|
|
1345
|
+
lines.push(' 📊 商业级数据流分析 (DataFlow Pro) ');
|
|
1346
|
+
lines.push('═══════════════════════════════════════════════════════════════');
|
|
1347
|
+
lines.push('');
|
|
1348
|
+
|
|
1349
|
+
lines.push(`📈 分析统计`);
|
|
1350
|
+
lines.push(` 基本块: ${result.statistics.blocksConstructed}`);
|
|
1351
|
+
lines.push(` 调用点: ${result.statistics.callSitesAnalyzed}`);
|
|
1352
|
+
lines.push(` 迭代次数: ${result.statistics.fixedPointIterations}`);
|
|
1353
|
+
lines.push(` 节点分析: ${result.statistics.nodesAnalyzed}`);
|
|
1354
|
+
lines.push(` 约束生成: ${result.statistics.constraintsGenerated}`);
|
|
1355
|
+
lines.push(` Promise解包: ${result.statistics.promiseUnwraps}`);
|
|
1356
|
+
lines.push(` 条件分支: ${result.statistics.conditionalBranches}`);
|
|
1357
|
+
lines.push(` 类型收窄: ${result.statistics.typesNarrowed}`);
|
|
1358
|
+
lines.push(` 污点值: ${result.statistics.taintedValues}`);
|
|
1359
|
+
lines.push(` 逃逸值: ${result.statistics.escapedValues}`);
|
|
1360
|
+
lines.push(` 置信度: ${result.confidence}`);
|
|
1361
|
+
lines.push(` 耗时: ${result.duration}ms`);
|
|
1362
|
+
lines.push('');
|
|
1363
|
+
|
|
1364
|
+
if (result.taintedPaths.length > 0) {
|
|
1365
|
+
lines.push('⚠️ 污点传播路径:');
|
|
1366
|
+
for (const tp of result.taintedPaths.slice(0, 5)) {
|
|
1367
|
+
lines.push(` ${tp.source} → ${tp.sink} (来源: ${tp.taintSource})`);
|
|
1368
|
+
}
|
|
1369
|
+
lines.push('');
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
if (result.typeNarrowing.size > 0) {
|
|
1373
|
+
lines.push('🔽 类型收窄:');
|
|
1374
|
+
for (const [varName, narrowing] of result.typeNarrowing) {
|
|
1375
|
+
lines.push(` ${varName}:`);
|
|
1376
|
+
for (const n of narrowing) {
|
|
1377
|
+
lines.push(` → 第${n.line}行: ${n.types.join(' | ')}`);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
lines.push('');
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
if (result.flowPaths.length > 0) {
|
|
1384
|
+
lines.push('🔗 数据流路径:');
|
|
1385
|
+
for (const fp of result.flowPaths.slice(0, 5)) {
|
|
1386
|
+
lines.push(` ${fp.source}`);
|
|
1387
|
+
lines.push(` → ${fp.sink} (${fp.typeAtSink})`);
|
|
1388
|
+
if (fp.isTainted) lines.push(` ⚠️ 污点数据`);
|
|
1389
|
+
}
|
|
1390
|
+
lines.push('');
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
if (result.hasDataLeaks) {
|
|
1394
|
+
lines.push('🔴 警告: 检测到数据泄漏风险!');
|
|
1395
|
+
lines.push('');
|
|
1396
|
+
} else {
|
|
1397
|
+
lines.push('✅ 未检测到数据泄漏');
|
|
1398
|
+
lines.push('');
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
return lines.join('\n');
|
|
1402
|
+
}
|
|
1403
|
+
}
|