agent-security-scanner-mcp 4.3.0 → 4.4.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/package.json +4 -1
- package/src/semantic-analyzer.js +1293 -0
- package/src/semantic-integration.js +301 -0
- package/src/tools/scan-project.js +28 -6
- package/src/utils/github-clone.js +227 -0
- package/src/utils/npm-download.js +265 -0
|
@@ -0,0 +1,1293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic Code Analysis Layer
|
|
3
|
+
*
|
|
4
|
+
* Builds Code Property Graphs (CPG) combining:
|
|
5
|
+
* - Control Flow Graph (CFG) - execution paths
|
|
6
|
+
* - Data Flow Graph (DFG) - data dependencies
|
|
7
|
+
* - AST - syntax structure
|
|
8
|
+
*
|
|
9
|
+
* Detects logic-level vulnerabilities that AST+regex miss:
|
|
10
|
+
* - Missing authentication checks
|
|
11
|
+
* - Race conditions
|
|
12
|
+
* - TOCTOU (Time-of-Check-Time-of-Use)
|
|
13
|
+
* - Unreachable code
|
|
14
|
+
* - Logic contradictions
|
|
15
|
+
* - Use-after-free patterns
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export class ControlFlowGraph {
|
|
19
|
+
constructor(ast, language) {
|
|
20
|
+
this.ast = ast;
|
|
21
|
+
this.language = language;
|
|
22
|
+
this.nodes = new Map(); // id -> CFGNode
|
|
23
|
+
this.edges = []; // {from, to, type, condition}
|
|
24
|
+
this.entryNode = null;
|
|
25
|
+
this.exitNode = null;
|
|
26
|
+
this.nodeCounter = 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build CFG from AST
|
|
31
|
+
*/
|
|
32
|
+
build() {
|
|
33
|
+
this.entryNode = this.createNode('entry', { type: 'entry' });
|
|
34
|
+
this.exitNode = this.createNode('exit', { type: 'exit' });
|
|
35
|
+
|
|
36
|
+
if (!this.ast) {
|
|
37
|
+
this.addEdge(this.entryNode.id, this.exitNode.id, 'sequential');
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const bodyNode = this.processNode(this.ast, this.entryNode.id);
|
|
42
|
+
if (bodyNode) {
|
|
43
|
+
this.addEdge(bodyNode, this.exitNode.id, 'sequential');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Process AST node and build CFG
|
|
51
|
+
*/
|
|
52
|
+
processNode(node, prevNodeId) {
|
|
53
|
+
if (!node || typeof node !== 'object') {
|
|
54
|
+
return prevNodeId;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const nodeType = node.type || node.kind;
|
|
58
|
+
|
|
59
|
+
switch (nodeType) {
|
|
60
|
+
case 'program':
|
|
61
|
+
case 'module':
|
|
62
|
+
case 'source_file':
|
|
63
|
+
return this.processSequence(node.body || node.children || [], prevNodeId);
|
|
64
|
+
|
|
65
|
+
case 'function_definition':
|
|
66
|
+
case 'function_declaration':
|
|
67
|
+
case 'arrow_function':
|
|
68
|
+
case 'method_definition':
|
|
69
|
+
return this.processFunctionDeclaration(node, prevNodeId);
|
|
70
|
+
|
|
71
|
+
case 'if_statement':
|
|
72
|
+
case 'if':
|
|
73
|
+
return this.processIfStatement(node, prevNodeId);
|
|
74
|
+
|
|
75
|
+
case 'while_statement':
|
|
76
|
+
case 'while':
|
|
77
|
+
case 'for_statement':
|
|
78
|
+
case 'for':
|
|
79
|
+
case 'for_in_statement':
|
|
80
|
+
case 'for_of_statement':
|
|
81
|
+
return this.processLoop(node, prevNodeId);
|
|
82
|
+
|
|
83
|
+
case 'try_statement':
|
|
84
|
+
case 'try':
|
|
85
|
+
return this.processTryStatement(node, prevNodeId);
|
|
86
|
+
|
|
87
|
+
case 'return_statement':
|
|
88
|
+
case 'return':
|
|
89
|
+
return this.processReturn(node, prevNodeId);
|
|
90
|
+
|
|
91
|
+
case 'throw_statement':
|
|
92
|
+
case 'throw':
|
|
93
|
+
return this.processThrow(node, prevNodeId);
|
|
94
|
+
|
|
95
|
+
case 'break_statement':
|
|
96
|
+
case 'break':
|
|
97
|
+
case 'continue_statement':
|
|
98
|
+
case 'continue':
|
|
99
|
+
return this.processBreakContinue(node, prevNodeId);
|
|
100
|
+
|
|
101
|
+
case 'switch_statement':
|
|
102
|
+
case 'switch':
|
|
103
|
+
return this.processSwitch(node, prevNodeId);
|
|
104
|
+
|
|
105
|
+
case 'expression_statement':
|
|
106
|
+
case 'call_expression':
|
|
107
|
+
case 'assignment_expression':
|
|
108
|
+
case 'variable_declaration':
|
|
109
|
+
return this.processStatement(node, prevNodeId);
|
|
110
|
+
|
|
111
|
+
case 'block_statement':
|
|
112
|
+
case 'block':
|
|
113
|
+
return this.processSequence(node.body || node.children || [], prevNodeId);
|
|
114
|
+
|
|
115
|
+
default:
|
|
116
|
+
// For unknown types, process children if available
|
|
117
|
+
if (Array.isArray(node.children)) {
|
|
118
|
+
return this.processSequence(node.children, prevNodeId);
|
|
119
|
+
}
|
|
120
|
+
return prevNodeId;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Process sequence of statements
|
|
126
|
+
*/
|
|
127
|
+
processSequence(statements, prevNodeId) {
|
|
128
|
+
let currentNode = prevNodeId;
|
|
129
|
+
for (const stmt of statements) {
|
|
130
|
+
if (currentNode === null) {
|
|
131
|
+
// Previous statement was terminal (return/throw/break/continue)
|
|
132
|
+
// Still process remaining statements to detect them as unreachable,
|
|
133
|
+
// but don't pass prevNodeId (they won't be connected)
|
|
134
|
+
this.processNode(stmt, null);
|
|
135
|
+
} else {
|
|
136
|
+
currentNode = this.processNode(stmt, currentNode);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return currentNode;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Process function declaration
|
|
144
|
+
*/
|
|
145
|
+
processFunctionDeclaration(node, prevNodeId) {
|
|
146
|
+
const funcNode = this.createNode('function', {
|
|
147
|
+
type: 'function',
|
|
148
|
+
name: this.extractFunctionName(node),
|
|
149
|
+
params: this.extractParams(node),
|
|
150
|
+
ast: node
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
this.addEdge(prevNodeId, funcNode.id, 'sequential');
|
|
154
|
+
|
|
155
|
+
// Process function body
|
|
156
|
+
const body = node.body;
|
|
157
|
+
if (body) {
|
|
158
|
+
const bodyEntry = this.createNode('block_entry', { type: 'block_entry' });
|
|
159
|
+
const bodyExit = this.createNode('block_exit', { type: 'block_exit' });
|
|
160
|
+
|
|
161
|
+
this.addEdge(funcNode.id, bodyEntry.id, 'function_enter');
|
|
162
|
+
const lastNode = this.processNode(body, bodyEntry.id);
|
|
163
|
+
this.addEdge(lastNode, bodyExit.id, 'sequential');
|
|
164
|
+
this.addEdge(bodyExit.id, funcNode.id, 'function_exit');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return funcNode.id;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Process if statement (creates branching)
|
|
172
|
+
*/
|
|
173
|
+
processIfStatement(node, prevNodeId) {
|
|
174
|
+
const condition = node.condition || node.test;
|
|
175
|
+
const consequent = node.consequent || node.then_clause;
|
|
176
|
+
const alternate = node.alternate || node.else_clause;
|
|
177
|
+
|
|
178
|
+
const conditionNode = this.createNode('condition', {
|
|
179
|
+
type: 'condition',
|
|
180
|
+
condition: this.extractExpression(condition),
|
|
181
|
+
ast: condition
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
this.addEdge(prevNodeId, conditionNode.id, 'sequential');
|
|
185
|
+
|
|
186
|
+
// True branch
|
|
187
|
+
const trueNode = this.processNode(consequent, conditionNode.id);
|
|
188
|
+
this.addEdge(conditionNode.id, trueNode, 'true_branch', this.extractExpression(condition));
|
|
189
|
+
|
|
190
|
+
// False branch (else or merge point)
|
|
191
|
+
let falseNode;
|
|
192
|
+
if (alternate) {
|
|
193
|
+
falseNode = this.processNode(alternate, conditionNode.id);
|
|
194
|
+
this.addEdge(conditionNode.id, falseNode, 'false_branch', `!(${this.extractExpression(condition)})`);
|
|
195
|
+
} else {
|
|
196
|
+
falseNode = conditionNode.id;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Merge point
|
|
200
|
+
const mergeNode = this.createNode('merge', { type: 'merge' });
|
|
201
|
+
this.addEdge(trueNode, mergeNode.id, 'merge');
|
|
202
|
+
this.addEdge(falseNode, mergeNode.id, 'merge');
|
|
203
|
+
|
|
204
|
+
return mergeNode.id;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Process loop statement
|
|
209
|
+
*/
|
|
210
|
+
processLoop(node, prevNodeId) {
|
|
211
|
+
const loopHeader = this.createNode('loop_header', {
|
|
212
|
+
type: 'loop_header',
|
|
213
|
+
loopType: node.type,
|
|
214
|
+
condition: this.extractExpression(node.condition || node.test),
|
|
215
|
+
ast: node
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
this.addEdge(prevNodeId, loopHeader.id, 'sequential');
|
|
219
|
+
|
|
220
|
+
const loopBody = this.processNode(node.body, loopHeader.id);
|
|
221
|
+
|
|
222
|
+
// Back edge (loop iteration)
|
|
223
|
+
this.addEdge(loopBody, loopHeader.id, 'back_edge');
|
|
224
|
+
|
|
225
|
+
// Exit edge (loop exit)
|
|
226
|
+
const exitNode = this.createNode('loop_exit', { type: 'loop_exit' });
|
|
227
|
+
this.addEdge(loopHeader.id, exitNode.id, 'loop_exit');
|
|
228
|
+
|
|
229
|
+
return exitNode.id;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Process try-catch-finally
|
|
234
|
+
*/
|
|
235
|
+
processTryStatement(node, prevNodeId) {
|
|
236
|
+
const tryBlock = node.body || node.try_clause;
|
|
237
|
+
const catchClause = node.handler || node.catch_clause;
|
|
238
|
+
const finallyBlock = node.finalizer || node.finally_clause;
|
|
239
|
+
|
|
240
|
+
const tryNode = this.createNode('try', { type: 'try', ast: node });
|
|
241
|
+
this.addEdge(prevNodeId, tryNode.id, 'sequential');
|
|
242
|
+
|
|
243
|
+
const tryBodyNode = this.processNode(tryBlock, tryNode.id);
|
|
244
|
+
|
|
245
|
+
let mergeNode = this.createNode('merge', { type: 'merge' });
|
|
246
|
+
|
|
247
|
+
// Normal path
|
|
248
|
+
this.addEdge(tryBodyNode, mergeNode.id, 'normal');
|
|
249
|
+
|
|
250
|
+
// Exception path
|
|
251
|
+
if (catchClause) {
|
|
252
|
+
const catchNode = this.processNode(catchClause.body, tryNode.id);
|
|
253
|
+
this.addEdge(tryNode.id, catchNode, 'exception');
|
|
254
|
+
this.addEdge(catchNode, mergeNode.id, 'merge');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Finally always executes
|
|
258
|
+
if (finallyBlock) {
|
|
259
|
+
const finallyNode = this.processNode(finallyBlock, mergeNode.id);
|
|
260
|
+
return finallyNode;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return mergeNode.id;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Process return statement
|
|
268
|
+
*/
|
|
269
|
+
processReturn(node, prevNodeId) {
|
|
270
|
+
const returnNode = this.createNode('return', {
|
|
271
|
+
type: 'return',
|
|
272
|
+
value: this.extractExpression(node.argument || node.value),
|
|
273
|
+
ast: node
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
this.addEdge(prevNodeId, returnNode.id, 'sequential');
|
|
277
|
+
this.addEdge(returnNode.id, this.exitNode.id, 'return');
|
|
278
|
+
|
|
279
|
+
// Return null to indicate no subsequent code should be connected
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Process throw statement
|
|
285
|
+
*/
|
|
286
|
+
processThrow(node, prevNodeId) {
|
|
287
|
+
const throwNode = this.createNode('throw', {
|
|
288
|
+
type: 'throw',
|
|
289
|
+
value: this.extractExpression(node.argument || node.value),
|
|
290
|
+
ast: node
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
this.addEdge(prevNodeId, throwNode.id, 'sequential');
|
|
294
|
+
this.addEdge(throwNode.id, this.exitNode.id, 'exception');
|
|
295
|
+
|
|
296
|
+
// Return null to indicate no subsequent code should be connected
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Process break/continue
|
|
302
|
+
*/
|
|
303
|
+
processBreakContinue(node, prevNodeId) {
|
|
304
|
+
const jumpNode = this.createNode(node.type, {
|
|
305
|
+
type: node.type,
|
|
306
|
+
label: node.label?.name,
|
|
307
|
+
ast: node
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
this.addEdge(prevNodeId, jumpNode.id, 'sequential');
|
|
311
|
+
// Note: Need to find target loop header/exit in post-processing
|
|
312
|
+
|
|
313
|
+
return jumpNode.id;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Process switch statement
|
|
318
|
+
*/
|
|
319
|
+
processSwitch(node, prevNodeId) {
|
|
320
|
+
const switchNode = this.createNode('switch', {
|
|
321
|
+
type: 'switch',
|
|
322
|
+
discriminant: this.extractExpression(node.discriminant || node.value),
|
|
323
|
+
ast: node
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
this.addEdge(prevNodeId, switchNode.id, 'sequential');
|
|
327
|
+
|
|
328
|
+
const cases = node.cases || node.children?.filter(c => c.type === 'switch_case') || [];
|
|
329
|
+
const mergeNode = this.createNode('merge', { type: 'merge' });
|
|
330
|
+
|
|
331
|
+
for (const caseNode of cases) {
|
|
332
|
+
const caseValue = caseNode.test || caseNode.value;
|
|
333
|
+
const caseBodyNode = this.processSequence(caseNode.consequent || caseNode.children || [], switchNode.id);
|
|
334
|
+
|
|
335
|
+
this.addEdge(switchNode.id, caseBodyNode, 'case', this.extractExpression(caseValue));
|
|
336
|
+
this.addEdge(caseBodyNode, mergeNode.id, 'merge');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return mergeNode.id;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Process regular statement
|
|
344
|
+
*/
|
|
345
|
+
processStatement(node, prevNodeId) {
|
|
346
|
+
const stmtNode = this.createNode('statement', {
|
|
347
|
+
type: 'statement',
|
|
348
|
+
statementType: node.type,
|
|
349
|
+
expression: this.extractExpression(node),
|
|
350
|
+
ast: node
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
this.addEdge(prevNodeId, stmtNode.id, 'sequential');
|
|
354
|
+
return stmtNode.id;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Create CFG node
|
|
359
|
+
*/
|
|
360
|
+
createNode(type, data = {}) {
|
|
361
|
+
const id = `node_${this.nodeCounter++}`;
|
|
362
|
+
const node = { id, type, ...data };
|
|
363
|
+
this.nodes.set(id, node);
|
|
364
|
+
return node;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Add CFG edge
|
|
369
|
+
*/
|
|
370
|
+
addEdge(fromId, toId, type = 'sequential', condition = null) {
|
|
371
|
+
if (!fromId || !toId) return;
|
|
372
|
+
this.edges.push({ from: fromId, to: toId, type, condition });
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Extract function name from node
|
|
377
|
+
*/
|
|
378
|
+
extractFunctionName(node) {
|
|
379
|
+
if (node.id?.name) return node.id.name;
|
|
380
|
+
if (node.name?.text) return node.name.text;
|
|
381
|
+
if (node.declarator?.name) return node.declarator.name;
|
|
382
|
+
return '<anonymous>';
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Extract function parameters
|
|
387
|
+
*/
|
|
388
|
+
extractParams(node) {
|
|
389
|
+
const params = node.parameters || node.params || [];
|
|
390
|
+
return params.map(p => {
|
|
391
|
+
if (typeof p === 'string') return p;
|
|
392
|
+
if (p.name?.text) return p.name.text;
|
|
393
|
+
if (p.text) return p.text;
|
|
394
|
+
return '<param>';
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Extract expression as string (simplified)
|
|
400
|
+
*/
|
|
401
|
+
extractExpression(node) {
|
|
402
|
+
if (!node) return '';
|
|
403
|
+
if (typeof node === 'string') return node;
|
|
404
|
+
if (node.text) return node.text;
|
|
405
|
+
if (node.name) return node.name;
|
|
406
|
+
if (node.type === 'identifier' && node.value) return node.value;
|
|
407
|
+
return `<${node.type || 'expr'}>`;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Get all paths from entry to exit
|
|
412
|
+
*/
|
|
413
|
+
getAllPaths(maxDepth = 100) {
|
|
414
|
+
const paths = [];
|
|
415
|
+
const visited = new Set();
|
|
416
|
+
|
|
417
|
+
const dfs = (nodeId, path, depth) => {
|
|
418
|
+
if (depth > maxDepth) return; // Prevent infinite loops
|
|
419
|
+
if (nodeId === this.exitNode.id) {
|
|
420
|
+
paths.push([...path]);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (visited.has(nodeId)) return;
|
|
424
|
+
|
|
425
|
+
visited.add(nodeId);
|
|
426
|
+
path.push(nodeId);
|
|
427
|
+
|
|
428
|
+
const outgoingEdges = this.edges.filter(e => e.from === nodeId);
|
|
429
|
+
for (const edge of outgoingEdges) {
|
|
430
|
+
dfs(edge.to, path, depth + 1);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
path.pop();
|
|
434
|
+
visited.delete(nodeId);
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
dfs(this.entryNode.id, [], 0);
|
|
438
|
+
return paths;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Find all reachable nodes from a given node
|
|
443
|
+
*/
|
|
444
|
+
getReachableNodes(startNodeId) {
|
|
445
|
+
const reachable = new Set();
|
|
446
|
+
const queue = [startNodeId];
|
|
447
|
+
|
|
448
|
+
while (queue.length > 0) {
|
|
449
|
+
const nodeId = queue.shift();
|
|
450
|
+
if (reachable.has(nodeId)) continue;
|
|
451
|
+
|
|
452
|
+
reachable.add(nodeId);
|
|
453
|
+
const outgoing = this.edges.filter(e => e.from === nodeId);
|
|
454
|
+
for (const edge of outgoing) {
|
|
455
|
+
queue.push(edge.to);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return reachable;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Find unreachable nodes (dead code)
|
|
464
|
+
*/
|
|
465
|
+
getUnreachableNodes() {
|
|
466
|
+
const reachable = this.getReachableNodes(this.entryNode.id);
|
|
467
|
+
const unreachable = [];
|
|
468
|
+
|
|
469
|
+
for (const [nodeId, node] of this.nodes) {
|
|
470
|
+
if (!reachable.has(nodeId) && nodeId !== this.exitNode.id) {
|
|
471
|
+
unreachable.push({ nodeId, node });
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return unreachable;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Export to DOT format for visualization
|
|
480
|
+
*/
|
|
481
|
+
toDot() {
|
|
482
|
+
let dot = 'digraph CFG {\n';
|
|
483
|
+
dot += ' rankdir=TB;\n';
|
|
484
|
+
dot += ' node [shape=box];\n\n';
|
|
485
|
+
|
|
486
|
+
// Nodes
|
|
487
|
+
for (const [id, node] of this.nodes) {
|
|
488
|
+
const label = this.getNodeLabel(node);
|
|
489
|
+
const shape = this.getNodeShape(node.type);
|
|
490
|
+
dot += ` ${id} [label="${label}", shape=${shape}];\n`;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
dot += '\n';
|
|
494
|
+
|
|
495
|
+
// Edges
|
|
496
|
+
for (const edge of this.edges) {
|
|
497
|
+
const style = this.getEdgeStyle(edge.type);
|
|
498
|
+
const label = edge.condition ? `[label="${edge.condition}"]` : '';
|
|
499
|
+
dot += ` ${edge.from} -> ${edge.to} ${label} ${style};\n`;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
dot += '}\n';
|
|
503
|
+
return dot;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
getNodeLabel(node) {
|
|
507
|
+
switch (node.type) {
|
|
508
|
+
case 'entry': return 'ENTRY';
|
|
509
|
+
case 'exit': return 'EXIT';
|
|
510
|
+
case 'function': return `func ${node.name}`;
|
|
511
|
+
case 'condition': return `if (${node.condition})`;
|
|
512
|
+
case 'loop_header': return `loop (${node.condition})`;
|
|
513
|
+
case 'statement': return node.expression || 'stmt';
|
|
514
|
+
case 'return': return `return ${node.value}`;
|
|
515
|
+
default: return node.type;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
getNodeShape(type) {
|
|
520
|
+
switch (type) {
|
|
521
|
+
case 'entry':
|
|
522
|
+
case 'exit':
|
|
523
|
+
return 'ellipse';
|
|
524
|
+
case 'condition':
|
|
525
|
+
return 'diamond';
|
|
526
|
+
case 'merge':
|
|
527
|
+
return 'circle';
|
|
528
|
+
default:
|
|
529
|
+
return 'box';
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
getEdgeStyle(type) {
|
|
534
|
+
switch (type) {
|
|
535
|
+
case 'true_branch':
|
|
536
|
+
return '[color=green]';
|
|
537
|
+
case 'false_branch':
|
|
538
|
+
return '[color=red]';
|
|
539
|
+
case 'exception':
|
|
540
|
+
return '[style=dashed, color=red]';
|
|
541
|
+
case 'back_edge':
|
|
542
|
+
return '[style=dashed, color=blue]';
|
|
543
|
+
default:
|
|
544
|
+
return '';
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Data Flow Graph - tracks data dependencies
|
|
551
|
+
*/
|
|
552
|
+
export class DataFlowGraph {
|
|
553
|
+
constructor(cfg, ast) {
|
|
554
|
+
this.cfg = cfg;
|
|
555
|
+
this.ast = ast;
|
|
556
|
+
this.definitions = new Map(); // variable -> Set of definition nodes
|
|
557
|
+
this.uses = new Map(); // variable -> Set of use nodes
|
|
558
|
+
this.reachingDefs = new Map(); // nodeId -> Map(variable -> Set of def nodes)
|
|
559
|
+
this.liveVars = new Map(); // nodeId -> Set of live variables
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Build DFG using reaching definitions analysis
|
|
564
|
+
*/
|
|
565
|
+
build() {
|
|
566
|
+
this.analyzeDefinitionsAndUses();
|
|
567
|
+
this.computeReachingDefinitions();
|
|
568
|
+
this.computeLiveVariables();
|
|
569
|
+
return this;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Extract definitions and uses from CFG nodes
|
|
574
|
+
*/
|
|
575
|
+
analyzeDefinitionsAndUses() {
|
|
576
|
+
for (const [nodeId, node] of this.cfg.nodes) {
|
|
577
|
+
if (!node.ast) continue;
|
|
578
|
+
|
|
579
|
+
const defs = this.extractDefinitions(node.ast);
|
|
580
|
+
const uses = this.extractUses(node.ast);
|
|
581
|
+
|
|
582
|
+
for (const varName of defs) {
|
|
583
|
+
if (!this.definitions.has(varName)) {
|
|
584
|
+
this.definitions.set(varName, new Set());
|
|
585
|
+
}
|
|
586
|
+
this.definitions.get(varName).add(nodeId);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
for (const varName of uses) {
|
|
590
|
+
if (!this.uses.has(varName)) {
|
|
591
|
+
this.uses.set(varName, new Set());
|
|
592
|
+
}
|
|
593
|
+
this.uses.get(varName).add(nodeId);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Extract variable definitions from AST node
|
|
600
|
+
*/
|
|
601
|
+
extractDefinitions(node) {
|
|
602
|
+
const defs = new Set();
|
|
603
|
+
if (!node || typeof node !== 'object') return defs;
|
|
604
|
+
|
|
605
|
+
const type = node.type || node.kind;
|
|
606
|
+
|
|
607
|
+
switch (type) {
|
|
608
|
+
case 'variable_declaration':
|
|
609
|
+
case 'lexical_declaration':
|
|
610
|
+
if (node.declarations) {
|
|
611
|
+
for (const decl of node.declarations) {
|
|
612
|
+
const name = this.extractVariableName(decl.id || decl.name);
|
|
613
|
+
if (name) defs.add(name);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
break;
|
|
617
|
+
|
|
618
|
+
case 'assignment_expression':
|
|
619
|
+
case 'assignment':
|
|
620
|
+
const leftName = this.extractVariableName(node.left || node.lhs);
|
|
621
|
+
if (leftName) defs.add(leftName);
|
|
622
|
+
break;
|
|
623
|
+
|
|
624
|
+
case 'update_expression':
|
|
625
|
+
const argName = this.extractVariableName(node.argument);
|
|
626
|
+
if (argName) defs.add(argName);
|
|
627
|
+
break;
|
|
628
|
+
|
|
629
|
+
case 'for_statement':
|
|
630
|
+
case 'for':
|
|
631
|
+
if (node.init) {
|
|
632
|
+
const initDefs = this.extractDefinitions(node.init);
|
|
633
|
+
initDefs.forEach(d => defs.add(d));
|
|
634
|
+
}
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return defs;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Extract variable uses from AST node
|
|
643
|
+
*/
|
|
644
|
+
extractUses(node) {
|
|
645
|
+
const uses = new Set();
|
|
646
|
+
if (!node || typeof node !== 'object') return uses;
|
|
647
|
+
|
|
648
|
+
// Simple heuristic: any identifier reference is a use
|
|
649
|
+
// (except in definition contexts which are handled separately)
|
|
650
|
+
this.walkNode(node, (n) => {
|
|
651
|
+
if ((n.type === 'identifier' || n.kind === 'identifier') && n.name) {
|
|
652
|
+
uses.add(n.name);
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
return uses;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Walk AST node recursively
|
|
661
|
+
*/
|
|
662
|
+
walkNode(node, callback) {
|
|
663
|
+
if (!node || typeof node !== 'object') return;
|
|
664
|
+
|
|
665
|
+
callback(node);
|
|
666
|
+
|
|
667
|
+
if (Array.isArray(node.children)) {
|
|
668
|
+
node.children.forEach(child => this.walkNode(child, callback));
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Common AST properties
|
|
672
|
+
const props = ['body', 'consequent', 'alternate', 'test', 'argument',
|
|
673
|
+
'left', 'right', 'object', 'property', 'callee', 'arguments', 'expression'];
|
|
674
|
+
|
|
675
|
+
for (const prop of props) {
|
|
676
|
+
if (node[prop]) {
|
|
677
|
+
if (Array.isArray(node[prop])) {
|
|
678
|
+
node[prop].forEach(child => this.walkNode(child, callback));
|
|
679
|
+
} else {
|
|
680
|
+
this.walkNode(node[prop], callback);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Extract variable name from node
|
|
688
|
+
*/
|
|
689
|
+
extractVariableName(node) {
|
|
690
|
+
if (!node) return null;
|
|
691
|
+
if (typeof node === 'string') return node;
|
|
692
|
+
if (node.name) return node.name;
|
|
693
|
+
if (node.text) return node.text;
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Compute reaching definitions (which definitions reach each node)
|
|
699
|
+
*/
|
|
700
|
+
computeReachingDefinitions() {
|
|
701
|
+
// Iterative worklist algorithm
|
|
702
|
+
const worklist = Array.from(this.cfg.nodes.keys());
|
|
703
|
+
|
|
704
|
+
// Initialize
|
|
705
|
+
for (const nodeId of this.cfg.nodes.keys()) {
|
|
706
|
+
this.reachingDefs.set(nodeId, new Map());
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
while (worklist.length > 0) {
|
|
710
|
+
const nodeId = worklist.shift();
|
|
711
|
+
const node = this.cfg.nodes.get(nodeId);
|
|
712
|
+
|
|
713
|
+
// IN[node] = Union of OUT[pred] for all predecessors
|
|
714
|
+
const inDefs = new Map();
|
|
715
|
+
const predecessors = this.cfg.edges.filter(e => e.to === nodeId);
|
|
716
|
+
|
|
717
|
+
for (const pred of predecessors) {
|
|
718
|
+
const predOut = this.reachingDefs.get(pred.from);
|
|
719
|
+
if (predOut) {
|
|
720
|
+
for (const [varName, defNodes] of predOut) {
|
|
721
|
+
if (!inDefs.has(varName)) {
|
|
722
|
+
inDefs.set(varName, new Set());
|
|
723
|
+
}
|
|
724
|
+
defNodes.forEach(d => inDefs.get(varName).add(d));
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// GEN[node] = definitions generated by this node
|
|
730
|
+
// KILL[node] = definitions killed by this node
|
|
731
|
+
const gen = new Map(inDefs);
|
|
732
|
+
const defs = this.extractDefinitions(node.ast || {});
|
|
733
|
+
|
|
734
|
+
for (const varName of defs) {
|
|
735
|
+
gen.set(varName, new Set([nodeId]));
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// OUT[node] = GEN[node] ∪ (IN[node] - KILL[node])
|
|
739
|
+
const oldOut = this.reachingDefs.get(nodeId);
|
|
740
|
+
this.reachingDefs.set(nodeId, gen);
|
|
741
|
+
|
|
742
|
+
// Check if changed
|
|
743
|
+
if (!this.mapsEqual(oldOut, gen)) {
|
|
744
|
+
// Add successors to worklist
|
|
745
|
+
const successors = this.cfg.edges.filter(e => e.from === nodeId);
|
|
746
|
+
for (const succ of successors) {
|
|
747
|
+
if (!worklist.includes(succ.to)) {
|
|
748
|
+
worklist.push(succ.to);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Compute live variables (which variables are live at each node)
|
|
757
|
+
*/
|
|
758
|
+
computeLiveVariables() {
|
|
759
|
+
// Backward analysis
|
|
760
|
+
const worklist = Array.from(this.cfg.nodes.keys());
|
|
761
|
+
|
|
762
|
+
for (const nodeId of this.cfg.nodes.keys()) {
|
|
763
|
+
this.liveVars.set(nodeId, new Set());
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
while (worklist.length > 0) {
|
|
767
|
+
const nodeId = worklist.shift();
|
|
768
|
+
const node = this.cfg.nodes.get(nodeId);
|
|
769
|
+
|
|
770
|
+
// OUT[node] = Union of IN[succ] for all successors
|
|
771
|
+
const outLive = new Set();
|
|
772
|
+
const successors = this.cfg.edges.filter(e => e.from === nodeId);
|
|
773
|
+
|
|
774
|
+
for (const succ of successors) {
|
|
775
|
+
const succIn = this.liveVars.get(succ.to);
|
|
776
|
+
if (succIn) {
|
|
777
|
+
succIn.forEach(v => outLive.add(v));
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// IN[node] = USE[node] ∪ (OUT[node] - DEF[node])
|
|
782
|
+
const uses = this.extractUses(node.ast || {});
|
|
783
|
+
const defs = this.extractDefinitions(node.ast || {});
|
|
784
|
+
|
|
785
|
+
const inLive = new Set(uses);
|
|
786
|
+
for (const v of outLive) {
|
|
787
|
+
if (!defs.has(v)) {
|
|
788
|
+
inLive.add(v);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
const oldIn = this.liveVars.get(nodeId);
|
|
793
|
+
this.liveVars.set(nodeId, inLive);
|
|
794
|
+
|
|
795
|
+
// Check if changed
|
|
796
|
+
if (!this.setsEqual(oldIn, inLive)) {
|
|
797
|
+
const predecessors = this.cfg.edges.filter(e => e.to === nodeId);
|
|
798
|
+
for (const pred of predecessors) {
|
|
799
|
+
if (!worklist.includes(pred.from)) {
|
|
800
|
+
worklist.push(pred.from);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Helper: compare two Maps of Sets
|
|
809
|
+
*/
|
|
810
|
+
mapsEqual(map1, map2) {
|
|
811
|
+
if (!map1 || !map2) return map1 === map2;
|
|
812
|
+
if (map1.size !== map2.size) return false;
|
|
813
|
+
|
|
814
|
+
for (const [key, set1] of map1) {
|
|
815
|
+
const set2 = map2.get(key);
|
|
816
|
+
if (!this.setsEqual(set1, set2)) return false;
|
|
817
|
+
}
|
|
818
|
+
return true;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Helper: compare two Sets
|
|
823
|
+
*/
|
|
824
|
+
setsEqual(set1, set2) {
|
|
825
|
+
if (!set1 || !set2) return set1 === set2;
|
|
826
|
+
if (set1.size !== set2.size) return false;
|
|
827
|
+
for (const item of set1) {
|
|
828
|
+
if (!set2.has(item)) return false;
|
|
829
|
+
}
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Find use-def chains (which definitions reach a use)
|
|
835
|
+
*/
|
|
836
|
+
getUseDefChain(nodeId, varName) {
|
|
837
|
+
const reachingDefs = this.reachingDefs.get(nodeId);
|
|
838
|
+
if (!reachingDefs) return [];
|
|
839
|
+
return Array.from(reachingDefs.get(varName) || []);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Find def-use chains (which uses are reached by a definition)
|
|
844
|
+
*/
|
|
845
|
+
getDefUseChain(defNodeId, varName) {
|
|
846
|
+
const uses = [];
|
|
847
|
+
for (const [nodeId, reachingDefs] of this.reachingDefs) {
|
|
848
|
+
const defs = reachingDefs.get(varName);
|
|
849
|
+
if (defs && defs.has(defNodeId)) {
|
|
850
|
+
const nodeUses = this.extractUses(this.cfg.nodes.get(nodeId).ast || {});
|
|
851
|
+
if (nodeUses.has(varName)) {
|
|
852
|
+
uses.push(nodeId);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return uses;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Code Property Graph - combines CFG and DFG
|
|
862
|
+
*/
|
|
863
|
+
export class CodePropertyGraph {
|
|
864
|
+
constructor(ast, language) {
|
|
865
|
+
this.ast = ast;
|
|
866
|
+
this.language = language;
|
|
867
|
+
this.cfg = null;
|
|
868
|
+
this.dfg = null;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
build() {
|
|
872
|
+
this.cfg = new ControlFlowGraph(this.ast, this.language).build();
|
|
873
|
+
this.dfg = new DataFlowGraph(this.cfg, this.ast).build();
|
|
874
|
+
return this;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Get CFG
|
|
879
|
+
*/
|
|
880
|
+
getControlFlowGraph() {
|
|
881
|
+
return this.cfg;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Get DFG
|
|
886
|
+
*/
|
|
887
|
+
getDataFlowGraph() {
|
|
888
|
+
return this.dfg;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Export combined graph to DOT
|
|
893
|
+
*/
|
|
894
|
+
toDot() {
|
|
895
|
+
return this.cfg.toDot();
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Semantic Pattern Matcher - detects security patterns in CPG
|
|
901
|
+
*/
|
|
902
|
+
export class SemanticPatternMatcher {
|
|
903
|
+
constructor(cpg) {
|
|
904
|
+
this.cpg = cpg;
|
|
905
|
+
this.findings = [];
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Run all semantic pattern checks
|
|
910
|
+
*/
|
|
911
|
+
analyze() {
|
|
912
|
+
this.findings = [];
|
|
913
|
+
|
|
914
|
+
this.detectUnreachableCode();
|
|
915
|
+
this.detectMissingAuthChecks();
|
|
916
|
+
this.detectRaceConditions();
|
|
917
|
+
this.detectTOCTOU();
|
|
918
|
+
this.detectLogicContradictions();
|
|
919
|
+
this.detectUseAfterFree();
|
|
920
|
+
this.detectNullDereference();
|
|
921
|
+
this.detectDeadStores();
|
|
922
|
+
|
|
923
|
+
return this.findings;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Detect unreachable code (dead code)
|
|
928
|
+
*/
|
|
929
|
+
detectUnreachableCode() {
|
|
930
|
+
const unreachable = this.cpg.cfg.getUnreachableNodes();
|
|
931
|
+
|
|
932
|
+
for (const { nodeId, node } of unreachable) {
|
|
933
|
+
if (node.type === 'statement' || node.type === 'function') {
|
|
934
|
+
this.findings.push({
|
|
935
|
+
ruleId: 'semantic.unreachable-code',
|
|
936
|
+
message: 'Unreachable code detected - this code will never execute',
|
|
937
|
+
severity: 'warning',
|
|
938
|
+
nodeId,
|
|
939
|
+
node,
|
|
940
|
+
category: 'dead-code',
|
|
941
|
+
confidence: 'high'
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Detect missing authentication checks before sensitive operations
|
|
949
|
+
*/
|
|
950
|
+
detectMissingAuthChecks() {
|
|
951
|
+
const sensitiveOps = ['db.delete', 'db.update', 'executeCommand', 'readFile', 'writeFile'];
|
|
952
|
+
|
|
953
|
+
for (const [nodeId, node] of this.cpg.cfg.nodes) {
|
|
954
|
+
if (node.type !== 'statement') continue;
|
|
955
|
+
|
|
956
|
+
const expr = node.expression || '';
|
|
957
|
+
const hasSensitiveOp = sensitiveOps.some(op => expr.includes(op));
|
|
958
|
+
|
|
959
|
+
if (hasSensitiveOp) {
|
|
960
|
+
// Check if there's an auth check in the path from entry to this node
|
|
961
|
+
const hasAuthCheck = this.hasAuthCheckInPath(this.cpg.cfg.entryNode.id, nodeId);
|
|
962
|
+
|
|
963
|
+
if (!hasAuthCheck) {
|
|
964
|
+
this.findings.push({
|
|
965
|
+
ruleId: 'semantic.missing-auth-check',
|
|
966
|
+
message: `Sensitive operation '${expr}' executed without authentication check`,
|
|
967
|
+
severity: 'error',
|
|
968
|
+
nodeId,
|
|
969
|
+
node,
|
|
970
|
+
category: 'auth-bypass',
|
|
971
|
+
confidence: 'medium'
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* Check if there's an auth check in path from start to end node
|
|
980
|
+
*/
|
|
981
|
+
hasAuthCheckInPath(startNodeId, endNodeId) {
|
|
982
|
+
const authPatterns = ['isAuthenticated', 'checkAuth', 'requireAuth', 'req.user', 'user.id'];
|
|
983
|
+
const visited = new Set();
|
|
984
|
+
|
|
985
|
+
const dfs = (nodeId) => {
|
|
986
|
+
if (nodeId === endNodeId) return false;
|
|
987
|
+
if (visited.has(nodeId)) return false;
|
|
988
|
+
visited.add(nodeId);
|
|
989
|
+
|
|
990
|
+
const node = this.cpg.cfg.nodes.get(nodeId);
|
|
991
|
+
if (node && node.expression) {
|
|
992
|
+
const hasAuth = authPatterns.some(pattern => node.expression.includes(pattern));
|
|
993
|
+
if (hasAuth) return true;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const edges = this.cpg.cfg.edges.filter(e => e.from === nodeId);
|
|
997
|
+
for (const edge of edges) {
|
|
998
|
+
if (dfs(edge.to)) return true;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
return false;
|
|
1002
|
+
};
|
|
1003
|
+
|
|
1004
|
+
return dfs(startNodeId);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Detect potential race conditions (concurrent access without locks)
|
|
1009
|
+
*/
|
|
1010
|
+
detectRaceConditions() {
|
|
1011
|
+
// Look for shared variable access without synchronization
|
|
1012
|
+
const sharedVarAccess = new Map(); // varName -> [nodeIds]
|
|
1013
|
+
|
|
1014
|
+
for (const [nodeId, node] of this.cpg.cfg.nodes) {
|
|
1015
|
+
const defs = this.cpg.dfg.extractDefinitions(node.ast || {});
|
|
1016
|
+
const uses = this.cpg.dfg.extractUses(node.ast || {});
|
|
1017
|
+
|
|
1018
|
+
for (const varName of [...defs, ...uses]) {
|
|
1019
|
+
if (!sharedVarAccess.has(varName)) {
|
|
1020
|
+
sharedVarAccess.set(varName, []);
|
|
1021
|
+
}
|
|
1022
|
+
sharedVarAccess.get(varName).push(nodeId);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// Check for variables accessed in multiple places without locks
|
|
1027
|
+
for (const [varName, accessNodes] of sharedVarAccess) {
|
|
1028
|
+
if (accessNodes.length < 2) continue;
|
|
1029
|
+
|
|
1030
|
+
const hasLock = this.hasLockProtection(accessNodes);
|
|
1031
|
+
|
|
1032
|
+
if (!hasLock && this.isLikelySharedState(varName)) {
|
|
1033
|
+
this.findings.push({
|
|
1034
|
+
ruleId: 'semantic.race-condition',
|
|
1035
|
+
message: `Potential race condition: variable '${varName}' accessed concurrently without synchronization`,
|
|
1036
|
+
severity: 'warning',
|
|
1037
|
+
category: 'concurrency',
|
|
1038
|
+
confidence: 'low',
|
|
1039
|
+
affectedNodes: accessNodes
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Check if accesses are protected by locks
|
|
1047
|
+
*/
|
|
1048
|
+
hasLockProtection(nodeIds) {
|
|
1049
|
+
const lockPatterns = ['lock(', 'mutex.', 'synchronized', 'Lock()', 'acquire()'];
|
|
1050
|
+
|
|
1051
|
+
for (const nodeId of nodeIds) {
|
|
1052
|
+
const node = this.cpg.cfg.nodes.get(nodeId);
|
|
1053
|
+
if (node && node.expression) {
|
|
1054
|
+
const hasLock = lockPatterns.some(pattern => node.expression.includes(pattern));
|
|
1055
|
+
if (hasLock) return true;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return false;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Heuristic: is this likely a shared state variable?
|
|
1063
|
+
*/
|
|
1064
|
+
isLikelySharedState(varName) {
|
|
1065
|
+
const sharedPatterns = ['cache', 'state', 'global', 'shared', 'counter', 'pool'];
|
|
1066
|
+
return sharedPatterns.some(pattern => varName.toLowerCase().includes(pattern));
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Detect TOCTOU (Time-of-Check-Time-of-Use) vulnerabilities
|
|
1071
|
+
*/
|
|
1072
|
+
detectTOCTOU() {
|
|
1073
|
+
// Look for check-then-use patterns with file operations
|
|
1074
|
+
const checkPatterns = ['exists', 'isFile', 'access', 'stat'];
|
|
1075
|
+
const usePatterns = ['readFile', 'writeFile', 'unlink', 'open'];
|
|
1076
|
+
|
|
1077
|
+
for (const path of this.cpg.cfg.getAllPaths(50)) {
|
|
1078
|
+
let checkNode = null;
|
|
1079
|
+
let checkVar = null;
|
|
1080
|
+
|
|
1081
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
1082
|
+
const nodeId = path[i];
|
|
1083
|
+
const node = this.cpg.cfg.nodes.get(nodeId);
|
|
1084
|
+
const expr = node.expression || '';
|
|
1085
|
+
|
|
1086
|
+
// Found a check
|
|
1087
|
+
const isCheck = checkPatterns.some(p => expr.includes(p));
|
|
1088
|
+
if (isCheck) {
|
|
1089
|
+
checkNode = node;
|
|
1090
|
+
checkVar = this.extractFilePathVar(expr);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// Found a use after check
|
|
1094
|
+
const isUse = usePatterns.some(p => expr.includes(p));
|
|
1095
|
+
if (isUse && checkNode && checkVar) {
|
|
1096
|
+
const useVar = this.extractFilePathVar(expr);
|
|
1097
|
+
|
|
1098
|
+
if (useVar === checkVar) {
|
|
1099
|
+
this.findings.push({
|
|
1100
|
+
ruleId: 'semantic.toctou',
|
|
1101
|
+
message: `TOCTOU vulnerability: file check at node ${checkNode.id} followed by use at node ${nodeId} - file state may change between check and use`,
|
|
1102
|
+
severity: 'error',
|
|
1103
|
+
category: 'race-condition',
|
|
1104
|
+
confidence: 'medium',
|
|
1105
|
+
checkNode: checkNode.id,
|
|
1106
|
+
useNode: nodeId
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/**
|
|
1115
|
+
* Extract file path variable from expression
|
|
1116
|
+
*/
|
|
1117
|
+
extractFilePathVar(expr) {
|
|
1118
|
+
const match = expr.match(/['"]([^'"]+)['"]/);
|
|
1119
|
+
return match ? match[1] : null;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/**
|
|
1123
|
+
* Detect logic contradictions (impossible conditions)
|
|
1124
|
+
*/
|
|
1125
|
+
detectLogicContradictions() {
|
|
1126
|
+
for (const path of this.cpg.cfg.getAllPaths(50)) {
|
|
1127
|
+
const conditions = new Set();
|
|
1128
|
+
|
|
1129
|
+
for (const nodeId of path) {
|
|
1130
|
+
const node = this.cpg.cfg.nodes.get(nodeId);
|
|
1131
|
+
|
|
1132
|
+
if (node.type === 'condition') {
|
|
1133
|
+
const cond = node.condition;
|
|
1134
|
+
|
|
1135
|
+
// Check if we already have the negation of this condition
|
|
1136
|
+
const negation = this.getNegation(cond);
|
|
1137
|
+
if (conditions.has(negation)) {
|
|
1138
|
+
this.findings.push({
|
|
1139
|
+
ruleId: 'semantic.logic-contradiction',
|
|
1140
|
+
message: `Logic contradiction detected: condition '${cond}' conflicts with earlier condition '${negation}'`,
|
|
1141
|
+
severity: 'warning',
|
|
1142
|
+
nodeId,
|
|
1143
|
+
category: 'logic-error',
|
|
1144
|
+
confidence: 'high'
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
conditions.add(cond);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* Get negation of condition
|
|
1156
|
+
*/
|
|
1157
|
+
getNegation(cond) {
|
|
1158
|
+
if (cond.startsWith('!(')) {
|
|
1159
|
+
return cond.slice(2, -1);
|
|
1160
|
+
}
|
|
1161
|
+
return `!(${cond})`;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* Detect use-after-free patterns (C/C++)
|
|
1166
|
+
*/
|
|
1167
|
+
detectUseAfterFree() {
|
|
1168
|
+
if (!['c', 'cpp'].includes(this.cpg.language)) return;
|
|
1169
|
+
|
|
1170
|
+
for (const [varName, defNodes] of this.cpg.dfg.definitions) {
|
|
1171
|
+
for (const defNodeId of defNodes) {
|
|
1172
|
+
const defNode = this.cpg.cfg.nodes.get(defNodeId);
|
|
1173
|
+
const expr = defNode.expression || '';
|
|
1174
|
+
|
|
1175
|
+
if (expr.includes('free(') || expr.includes('delete ')) {
|
|
1176
|
+
// Check if variable is used after free
|
|
1177
|
+
const useNodes = this.cpg.dfg.getDefUseChain(defNodeId, varName);
|
|
1178
|
+
|
|
1179
|
+
if (useNodes.length > 0) {
|
|
1180
|
+
this.findings.push({
|
|
1181
|
+
ruleId: 'semantic.use-after-free',
|
|
1182
|
+
message: `Use-after-free detected: variable '${varName}' used after being freed`,
|
|
1183
|
+
severity: 'error',
|
|
1184
|
+
category: 'memory-safety',
|
|
1185
|
+
confidence: 'high',
|
|
1186
|
+
freeNode: defNodeId,
|
|
1187
|
+
useNodes
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/**
|
|
1196
|
+
* Detect null pointer dereference
|
|
1197
|
+
*/
|
|
1198
|
+
detectNullDereference() {
|
|
1199
|
+
for (const [nodeId, node] of this.cpg.cfg.nodes) {
|
|
1200
|
+
const expr = node.expression || '';
|
|
1201
|
+
|
|
1202
|
+
// Look for null checks followed by dereference
|
|
1203
|
+
if (expr.includes('null') || expr.includes('nil') || expr.includes('None')) {
|
|
1204
|
+
const edges = this.cpg.cfg.edges.filter(e => e.from === nodeId && e.type === 'true_branch');
|
|
1205
|
+
|
|
1206
|
+
for (const edge of edges) {
|
|
1207
|
+
const targetNode = this.cpg.cfg.nodes.get(edge.to);
|
|
1208
|
+
if (targetNode && (targetNode.expression || '').includes('.')) {
|
|
1209
|
+
this.findings.push({
|
|
1210
|
+
ruleId: 'semantic.null-dereference',
|
|
1211
|
+
message: 'Potential null pointer dereference after null check',
|
|
1212
|
+
severity: 'warning',
|
|
1213
|
+
category: 'null-safety',
|
|
1214
|
+
confidence: 'low',
|
|
1215
|
+
checkNode: nodeId,
|
|
1216
|
+
derefNode: edge.to
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* Detect dead stores (assignments to variables never used)
|
|
1226
|
+
*/
|
|
1227
|
+
detectDeadStores() {
|
|
1228
|
+
for (const [varName, defNodes] of this.cpg.dfg.definitions) {
|
|
1229
|
+
for (const defNodeId of defNodes) {
|
|
1230
|
+
const useNodes = this.cpg.dfg.getDefUseChain(defNodeId, varName);
|
|
1231
|
+
|
|
1232
|
+
if (useNodes.length === 0) {
|
|
1233
|
+
const defNode = this.cpg.cfg.nodes.get(defNodeId);
|
|
1234
|
+
|
|
1235
|
+
this.findings.push({
|
|
1236
|
+
ruleId: 'semantic.dead-store',
|
|
1237
|
+
message: `Dead store: assignment to variable '${varName}' is never used`,
|
|
1238
|
+
severity: 'info',
|
|
1239
|
+
category: 'optimization',
|
|
1240
|
+
confidence: 'high',
|
|
1241
|
+
nodeId: defNodeId
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/**
|
|
1250
|
+
* Main Semantic Analyzer
|
|
1251
|
+
*/
|
|
1252
|
+
export class SemanticAnalyzer {
|
|
1253
|
+
constructor(ast, language, filePath) {
|
|
1254
|
+
this.ast = ast;
|
|
1255
|
+
this.language = language;
|
|
1256
|
+
this.filePath = filePath;
|
|
1257
|
+
this.cpg = null;
|
|
1258
|
+
this.findings = [];
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* Run semantic analysis
|
|
1263
|
+
*/
|
|
1264
|
+
analyze() {
|
|
1265
|
+
try {
|
|
1266
|
+
// Build CPG
|
|
1267
|
+
this.cpg = new CodePropertyGraph(this.ast, this.language).build();
|
|
1268
|
+
|
|
1269
|
+
// Run pattern matching
|
|
1270
|
+
const matcher = new SemanticPatternMatcher(this.cpg);
|
|
1271
|
+
this.findings = matcher.analyze();
|
|
1272
|
+
|
|
1273
|
+
return this.findings;
|
|
1274
|
+
} catch (error) {
|
|
1275
|
+
console.error(`Semantic analysis error: ${error.message}`);
|
|
1276
|
+
return [];
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/**
|
|
1281
|
+
* Get Code Property Graph
|
|
1282
|
+
*/
|
|
1283
|
+
getCPG() {
|
|
1284
|
+
return this.cpg;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* Export CPG to DOT format for visualization
|
|
1289
|
+
*/
|
|
1290
|
+
exportDot() {
|
|
1291
|
+
return this.cpg ? this.cpg.toDot() : '';
|
|
1292
|
+
}
|
|
1293
|
+
}
|