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.
@@ -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
+ }