@orionscanner/cli 0.1.0

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.
Files changed (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +248 -0
  3. package/assets/logo.png +0 -0
  4. package/bin/orion.js +48 -0
  5. package/docs/API_REFERENCE.md +211 -0
  6. package/docs/ARCHITECTURE.md +183 -0
  7. package/docs/CHANGELOG.md +112 -0
  8. package/docs/CONTRIBUTING.md +157 -0
  9. package/docs/ENGINE_CATALOG.md +87 -0
  10. package/docs/GETTING_STARTED.md +226 -0
  11. package/package.json +48 -0
  12. package/pyproject.toml +16 -0
  13. package/src/orion/__init__.py +0 -0
  14. package/src/orion/__pycache__/__init__.cpython-313.pyc +0 -0
  15. package/src/orion/engine/__init__.py +17 -0
  16. package/src/orion/engine/__pycache__/__init__.cpython-313.pyc +0 -0
  17. package/src/orion/engine/__pycache__/config.cpython-313.pyc +0 -0
  18. package/src/orion/engine/__pycache__/core.cpython-313.pyc +0 -0
  19. package/src/orion/engine/__pycache__/ir.cpython-313.pyc +0 -0
  20. package/src/orion/engine/__pycache__/languages.cpython-313.pyc +0 -0
  21. package/src/orion/engine/__pycache__/registry.cpython-313.pyc +0 -0
  22. package/src/orion/engine/__pycache__/results.cpython-313.pyc +0 -0
  23. package/src/orion/engine/__pycache__/runner.cpython-313.pyc +0 -0
  24. package/src/orion/engine/api_security/__pycache__/engine.cpython-313.pyc +0 -0
  25. package/src/orion/engine/api_security/engine.py +125 -0
  26. package/src/orion/engine/architecture/__pycache__/engine.cpython-313.pyc +0 -0
  27. package/src/orion/engine/architecture/engine.py +463 -0
  28. package/src/orion/engine/authorization/__pycache__/engine.cpython-313.pyc +0 -0
  29. package/src/orion/engine/authorization/engine.py +337 -0
  30. package/src/orion/engine/callgraph/__pycache__/engine.cpython-313.pyc +0 -0
  31. package/src/orion/engine/callgraph/engine.py +236 -0
  32. package/src/orion/engine/cfg/__pycache__/engine.cpython-313.pyc +0 -0
  33. package/src/orion/engine/cfg/engine.py +509 -0
  34. package/src/orion/engine/chaos/__pycache__/engine.cpython-313.pyc +0 -0
  35. package/src/orion/engine/chaos/engine.py +268 -0
  36. package/src/orion/engine/chaos_config/__pycache__/engine.cpython-313.pyc +0 -0
  37. package/src/orion/engine/chaos_config/engine.py +322 -0
  38. package/src/orion/engine/complexity/__pycache__/engine.cpython-313.pyc +0 -0
  39. package/src/orion/engine/complexity/engine.py +144 -0
  40. package/src/orion/engine/compliance/__pycache__/engine.cpython-313.pyc +0 -0
  41. package/src/orion/engine/compliance/engine.py +325 -0
  42. package/src/orion/engine/config.py +41 -0
  43. package/src/orion/engine/config_engine/__pycache__/engine.cpython-313.pyc +0 -0
  44. package/src/orion/engine/config_engine/engine.py +364 -0
  45. package/src/orion/engine/config_validation/__pycache__/engine.cpython-313.pyc +0 -0
  46. package/src/orion/engine/config_validation/engine.py +313 -0
  47. package/src/orion/engine/container/__pycache__/engine.cpython-313.pyc +0 -0
  48. package/src/orion/engine/container/engine.py +546 -0
  49. package/src/orion/engine/contract_probe/__pycache__/engine.cpython-313.pyc +0 -0
  50. package/src/orion/engine/contract_probe/engine.py +316 -0
  51. package/src/orion/engine/core.py +75 -0
  52. package/src/orion/engine/crypto_guard/__pycache__/engine.cpython-313.pyc +0 -0
  53. package/src/orion/engine/crypto_guard/engine.py +134 -0
  54. package/src/orion/engine/data_residency/__pycache__/engine.cpython-313.pyc +0 -0
  55. package/src/orion/engine/data_residency/engine.py +238 -0
  56. package/src/orion/engine/dataflow/__pycache__/engine.cpython-313.pyc +0 -0
  57. package/src/orion/engine/dataflow/engine.py +398 -0
  58. package/src/orion/engine/dead_import/__pycache__/engine.cpython-313.pyc +0 -0
  59. package/src/orion/engine/dead_import/engine.py +293 -0
  60. package/src/orion/engine/deps/__pycache__/engine.cpython-313.pyc +0 -0
  61. package/src/orion/engine/deps/engine.py +244 -0
  62. package/src/orion/engine/dispatcher.py +350 -0
  63. package/src/orion/engine/docs/AUDIT_PROMPT.md +1248 -0
  64. package/src/orion/engine/duplication/__pycache__/engine.cpython-313.pyc +0 -0
  65. package/src/orion/engine/duplication/engine.py +120 -0
  66. package/src/orion/engine/error_handling/__pycache__/engine.cpython-313.pyc +0 -0
  67. package/src/orion/engine/error_handling/engine.py +256 -0
  68. package/src/orion/engine/fp_learn/__pycache__/engine.cpython-313.pyc +0 -0
  69. package/src/orion/engine/fp_learn/engine.py +248 -0
  70. package/src/orion/engine/hunter/__pycache__/engine.cpython-313.pyc +0 -0
  71. package/src/orion/engine/hunter/engine.py +324 -0
  72. package/src/orion/engine/iac_security/__pycache__/engine.cpython-313.pyc +0 -0
  73. package/src/orion/engine/iac_security/engine.py +218 -0
  74. package/src/orion/engine/ir.py +22 -0
  75. package/src/orion/engine/languages.py +454 -0
  76. package/src/orion/engine/license/__pycache__/engine.cpython-313.pyc +0 -0
  77. package/src/orion/engine/license/engine.py +172 -0
  78. package/src/orion/engine/lint/__pycache__/engine.cpython-313.pyc +0 -0
  79. package/src/orion/engine/lint/engine.py +558 -0
  80. package/src/orion/engine/llm_security/__pycache__/engine.cpython-313.pyc +0 -0
  81. package/src/orion/engine/llm_security/engine.py +125 -0
  82. package/src/orion/engine/logging_c/__pycache__/engine.cpython-313.pyc +0 -0
  83. package/src/orion/engine/logging_c/engine.py +164 -0
  84. package/src/orion/engine/noise/__pycache__/engine.cpython-313.pyc +0 -0
  85. package/src/orion/engine/noise/engine.py +140 -0
  86. package/src/orion/engine/orphan/__pycache__/engine.cpython-313.pyc +0 -0
  87. package/src/orion/engine/orphan/engine.py +244 -0
  88. package/src/orion/engine/perf_guard/__pycache__/engine.cpython-313.pyc +0 -0
  89. package/src/orion/engine/perf_guard/engine.py +345 -0
  90. package/src/orion/engine/prompt_security/__pycache__/engine.cpython-313.pyc +0 -0
  91. package/src/orion/engine/prompt_security/detectors/__init__.py +4 -0
  92. package/src/orion/engine/prompt_security/engine.py +170 -0
  93. package/src/orion/engine/provenance/__pycache__/engine.cpython-313.pyc +0 -0
  94. package/src/orion/engine/provenance/engine.py +242 -0
  95. package/src/orion/engine/registry.py +55 -0
  96. package/src/orion/engine/regression/__pycache__/engine.cpython-313.pyc +0 -0
  97. package/src/orion/engine/regression/engine.py +213 -0
  98. package/src/orion/engine/results.py +21 -0
  99. package/src/orion/engine/runner.py +1310 -0
  100. package/src/orion/engine/sandbox_escape/__pycache__/engine.cpython-313.pyc +0 -0
  101. package/src/orion/engine/sandbox_escape/engine.py +284 -0
  102. package/src/orion/engine/sast/__pycache__/engine.cpython-313.pyc +0 -0
  103. package/src/orion/engine/sast/engine.py +217 -0
  104. package/src/orion/engine/sast/patterns/__init__.py +513 -0
  105. package/src/orion/engine/sast/patterns/__pycache__/__init__.cpython-313.pyc +0 -0
  106. package/src/orion/engine/secrets/__pycache__/engine.cpython-313.pyc +0 -0
  107. package/src/orion/engine/secrets/engine.py +77 -0
  108. package/src/orion/engine/secrets/patterns/__init__.py +74 -0
  109. package/src/orion/engine/secrets/patterns/__pycache__/__init__.cpython-313.pyc +0 -0
  110. package/src/orion/engine/sensitive_data/__pycache__/engine.cpython-313.pyc +0 -0
  111. package/src/orion/engine/sensitive_data/engine.py +143 -0
  112. package/src/orion/engine/state/__pycache__/engine.cpython-313.pyc +0 -0
  113. package/src/orion/engine/state/engine.py +218 -0
  114. package/src/orion/engine/supply_chain_sbom/__pycache__/engine.cpython-313.pyc +0 -0
  115. package/src/orion/engine/supply_chain_sbom/engine.py +290 -0
  116. package/src/orion/engine/symbol/__pycache__/engine.cpython-313.pyc +0 -0
  117. package/src/orion/engine/symbol/engine.py +463 -0
  118. package/src/orion/engine/taint/__pycache__/engine.cpython-313.pyc +0 -0
  119. package/src/orion/engine/taint/engine.py +464 -0
  120. package/src/orion/engine/test_coverage/__pycache__/engine.cpython-313.pyc +0 -0
  121. package/src/orion/engine/test_coverage/engine.py +445 -0
  122. package/src/orion/engine/threat_model/__pycache__/engine.cpython-313.pyc +0 -0
  123. package/src/orion/engine/threat_model/engine.py +126 -0
  124. package/src/orion/engine/wiring_trace/__pycache__/engine.cpython-313.pyc +0 -0
  125. package/src/orion/engine/wiring_trace/engine.py +200 -0
  126. package/src/orion_engine.egg-info/PKG-INFO +7 -0
  127. package/src/orion_engine.egg-info/SOURCES.txt +65 -0
  128. package/src/orion_engine.egg-info/dependency_links.txt +1 -0
  129. package/src/orion_engine.egg-info/entry_points.txt +2 -0
  130. package/src/orion_engine.egg-info/top_level.txt +1 -0
@@ -0,0 +1,509 @@
1
+ """Control Flow Graph engine for detecting unreachable code, infinite loops, missing returns, and complex branches."""
2
+ import ast
3
+ from typing import Dict, List, Set, Optional, Any, Tuple
4
+ from dataclasses import dataclass, field
5
+ from collections import defaultdict
6
+ from orion.engine.config import EngineConfig, ResourceLimits
7
+ from orion.engine.ir import OrionIR
8
+ from orion.engine.results import EngineResult, Finding
9
+
10
+
11
+ @dataclass
12
+ class CFGNode:
13
+ id: int
14
+ lineno: int
15
+ end_lineno: int
16
+ node_type: str
17
+ ast_node: ast.AST
18
+ predecessors: List[int] = field(default_factory=list)
19
+ successors: List[int] = field(default_factory=list)
20
+ reachable: bool = True
21
+
22
+
23
+ @dataclass
24
+ class FunctionCFG:
25
+ name: str
26
+ nodes: List[CFGNode] = field(default_factory=list)
27
+ entry: int = 0
28
+ exits: List[int] = field(default_factory=list)
29
+ has_return: bool = False
30
+ is_generator: bool = False
31
+ is_async: bool = False
32
+ complexity: int = 1
33
+
34
+
35
+ class CFGBuilder(ast.NodeVisitor):
36
+ def __init__(self, filename: str, source_lines: List[str]):
37
+ self.filename = filename
38
+ self.source_lines = source_lines
39
+ self.findings: List[Finding] = []
40
+ self.current_function: Optional[FunctionCFG] = None
41
+ self.function_cfgs: Dict[str, FunctionCFG] = {}
42
+ self.node_counter = 0
43
+ self.current_class: Optional[str] = None
44
+ self.loop_stack: List[int] = []
45
+ self.try_stack: List[int] = []
46
+
47
+ def get_snippet(self, node: ast.AST, context: int = 2) -> str:
48
+ start = max(0, node.lineno - 1 - context)
49
+ end = min(len(self.source_lines),
50
+ getattr(node, 'end_lineno', node.lineno) + context if hasattr(node, 'end_lineno') else node.lineno + context)
51
+ return "\n".join(self.source_lines[start:end])
52
+
53
+ def new_node(self, node: ast.AST, node_type: str) -> CFGNode:
54
+ cfg_node = CFGNode(
55
+ id=self.node_counter,
56
+ lineno=node.lineno,
57
+ end_lineno=getattr(node, 'end_lineno', node.lineno),
58
+ node_type=node_type,
59
+ ast_node=node
60
+ )
61
+ self.node_counter += 1
62
+ return cfg_node
63
+
64
+ def add_edge(self, from_id: int, to_id: int, cfg: FunctionCFG):
65
+ cfg.nodes[from_id].successors.append(to_id)
66
+ cfg.nodes[to_id].predecessors.append(from_id)
67
+
68
+ def visit_ClassDef(self, node: ast.ClassDef):
69
+ old_class = self.current_class
70
+ self.current_class = node.name
71
+ self.generic_visit(node)
72
+ self.current_class = old_class
73
+
74
+ def visit_FunctionDef(self, node: ast.FunctionDef):
75
+ self._visit_function(node, is_async=False)
76
+
77
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
78
+ self._visit_function(node, is_async=True)
79
+
80
+ def _visit_function(self, node: ast.FunctionDef, is_async: bool):
81
+ old_function = self.current_function
82
+ old_counter = self.node_counter
83
+
84
+ qual_name = f"{self.current_class}.{node.name}" if self.current_class else node.name
85
+
86
+ cfg = FunctionCFG(name=qual_name, is_async=is_async)
87
+ self.current_function = cfg
88
+ self.node_counter = 0
89
+
90
+ entry_node = self.new_node(node, "entry")
91
+ cfg.nodes.append(entry_node)
92
+ cfg.entry = 0
93
+
94
+ prev_node = entry_node
95
+
96
+ for stmt in node.body:
97
+ stmt_node = self.new_node(stmt, type(stmt).__name__)
98
+ cfg.nodes.append(stmt_node)
99
+ self.add_edge(prev_node.id, stmt_node.id, cfg)
100
+ self._visit_statement(stmt, cfg, stmt_node.id)
101
+ prev_node = stmt_node
102
+
103
+ exit_node = self.new_node(node, "exit")
104
+ cfg.nodes.append(exit_node)
105
+ self.add_edge(prev_node.id, exit_node.id, cfg)
106
+ cfg.exits.append(exit_node.id)
107
+
108
+ self._analyze_function(cfg)
109
+ self.function_cfgs[qual_name] = cfg
110
+
111
+ self.current_function = old_function
112
+ self.node_counter = old_counter
113
+
114
+ def _visit_statement(self, node: ast.AST, cfg: FunctionCFG, node_id: int):
115
+ if isinstance(node, ast.If):
116
+ self._visit_if(node, cfg, node_id)
117
+ elif isinstance(node, ast.For):
118
+ self._visit_for(node, cfg, node_id)
119
+ elif isinstance(node, ast.While):
120
+ self._visit_while(node, cfg, node_id)
121
+ elif isinstance(node, ast.Try):
122
+ self._visit_try(node, cfg, node_id)
123
+ elif isinstance(node, ast.With):
124
+ self._visit_with(node, cfg, node_id)
125
+ elif isinstance(node, ast.Return):
126
+ cfg.has_return = True
127
+ elif isinstance(node, ast.Yield) or isinstance(node, ast.YieldFrom):
128
+ cfg.is_generator = True
129
+ elif isinstance(node, ast.Raise):
130
+ pass
131
+ elif isinstance(node, ast.Break) or isinstance(node, ast.Continue):
132
+ pass
133
+ else:
134
+ for child in ast.iter_child_nodes(node):
135
+ self._visit_statement(child, cfg, node_id)
136
+
137
+ def _visit_if(self, node: ast.If, cfg: FunctionCFG, node_id: int):
138
+ cfg.complexity += 1
139
+ test_node = self.new_node(node.test, "if_test")
140
+ cfg.nodes.append(test_node)
141
+ self.add_edge(node_id, test_node.id, cfg)
142
+
143
+ then_start = self.new_node(node, "if_then_start")
144
+ cfg.nodes.append(then_start)
145
+ self.add_edge(test_node.id, then_start.id, cfg)
146
+
147
+ else_start = self.new_node(node, "if_else_start")
148
+ cfg.nodes.append(else_start)
149
+ self.add_edge(test_node.id, else_start.id, cfg)
150
+
151
+ prev_then = then_start
152
+ for stmt in node.body:
153
+ stmt_node = self.new_node(stmt, type(stmt).__name__)
154
+ cfg.nodes.append(stmt_node)
155
+ self.add_edge(prev_then.id, stmt_node.id, cfg)
156
+ self._visit_statement(stmt, cfg, stmt_node.id)
157
+ prev_then = stmt_node
158
+
159
+ prev_else = else_start
160
+ has_else = bool(node.orelse)
161
+ for stmt in node.orelse:
162
+ stmt_node = self.new_node(stmt, type(stmt).__name__)
163
+ cfg.nodes.append(stmt_node)
164
+ self.add_edge(prev_else.id, stmt_node.id, cfg)
165
+ self._visit_statement(stmt, cfg, stmt_node.id)
166
+ prev_else = stmt_node
167
+
168
+ merge_node = self.new_node(node, "if_merge")
169
+ cfg.nodes.append(merge_node)
170
+ self.add_edge(prev_then.id, merge_node.id, cfg)
171
+ self.add_edge(prev_else.id, merge_node.id, cfg)
172
+
173
+ def _visit_for(self, node: ast.For, cfg: FunctionCFG, node_id: int):
174
+ cfg.complexity += 1
175
+ self.loop_stack.append(node_id)
176
+
177
+ iter_node = self.new_node(node.iter, "for_iter")
178
+ cfg.nodes.append(iter_node)
179
+ self.add_edge(node_id, iter_node.id, cfg)
180
+
181
+ loop_start = self.new_node(node, "for_loop_start")
182
+ cfg.nodes.append(loop_start)
183
+ self.add_edge(iter_node.id, loop_start.id, cfg)
184
+
185
+ body_start = self.new_node(node, "for_body_start")
186
+ cfg.nodes.append(body_start)
187
+ self.add_edge(loop_start.id, body_start.id, cfg)
188
+
189
+ prev = body_start
190
+ for stmt in node.body:
191
+ stmt_node = self.new_node(stmt, type(stmt).__name__)
192
+ cfg.nodes.append(stmt_node)
193
+ self.add_edge(prev.id, stmt_node.id, cfg)
194
+ self._visit_statement(stmt, cfg, stmt_node.id)
195
+ prev = stmt_node
196
+
197
+ self.add_edge(prev.id, loop_start.id, cfg)
198
+
199
+ else_start = self.new_node(node, "for_else_start")
200
+ cfg.nodes.append(else_start)
201
+ self.add_edge(loop_start.id, else_start.id, cfg)
202
+
203
+ prev_else = else_start
204
+ for stmt in node.orelse:
205
+ stmt_node = self.new_node(stmt, type(stmt).__name__)
206
+ cfg.nodes.append(stmt_node)
207
+ self.add_edge(prev_else.id, stmt_node.id, cfg)
208
+ self._visit_statement(stmt, cfg, stmt_node.id)
209
+ prev_else = stmt_node
210
+
211
+ merge_node = self.new_node(node, "for_merge")
212
+ cfg.nodes.append(merge_node)
213
+ self.add_edge(prev.id, merge_node.id, cfg)
214
+ self.add_edge(prev_else.id, merge_node.id, cfg)
215
+
216
+ self.loop_stack.pop()
217
+
218
+ def _visit_while(self, node: ast.While, cfg: FunctionCFG, node_id: int):
219
+ cfg.complexity += 1
220
+ self.loop_stack.append(node_id)
221
+
222
+ test_node = self.new_node(node.test, "while_test")
223
+ cfg.nodes.append(test_node)
224
+ self.add_edge(node_id, test_node.id, cfg)
225
+
226
+ body_start = self.new_node(node, "while_body_start")
227
+ cfg.nodes.append(body_start)
228
+ self.add_edge(test_node.id, body_start.id, cfg)
229
+
230
+ prev = body_start
231
+ for stmt in node.body:
232
+ stmt_node = self.new_node(stmt, type(stmt).__name__)
233
+ cfg.nodes.append(stmt_node)
234
+ self.add_edge(prev.id, stmt_node.id, cfg)
235
+ self._visit_statement(stmt, cfg, stmt_node.id)
236
+ prev = stmt_node
237
+
238
+ self.add_edge(prev.id, test_node.id, cfg)
239
+
240
+ else_start = self.new_node(node, "while_else_start")
241
+ cfg.nodes.append(else_start)
242
+ self.add_edge(test_node.id, else_start.id, cfg)
243
+
244
+ prev_else = else_start
245
+ for stmt in node.orelse:
246
+ stmt_node = self.new_node(stmt, type(stmt).__name__)
247
+ cfg.nodes.append(stmt_node)
248
+ self.add_edge(prev_else.id, stmt_node.id, cfg)
249
+ self._visit_statement(stmt, cfg, stmt_node.id)
250
+ prev_else = stmt_node
251
+
252
+ merge_node = self.new_node(node, "while_merge")
253
+ cfg.nodes.append(merge_node)
254
+ self.add_edge(prev.id, merge_node.id, cfg)
255
+ self.add_edge(prev_else.id, merge_node.id, cfg)
256
+
257
+ if self._is_infinite_loop(node):
258
+ self.findings.append(Finding(
259
+ engine="cfg",
260
+ severity="medium",
261
+ title="Potential infinite loop",
262
+ description=f"While loop with constant True condition and no break statement detected",
263
+ location=f"{self.filename}:{node.lineno}",
264
+ metadata={
265
+ "snippet": self.get_snippet(node),
266
+ "cwe": "CWE-835",
267
+ "confidence": 0.8,
268
+ "check": "infinite_loop",
269
+ }
270
+ ))
271
+
272
+ self.loop_stack.pop()
273
+
274
+ def _is_infinite_loop(self, node: ast.While) -> bool:
275
+ if isinstance(node.test, ast.Constant) and node.test.value is True:
276
+ for stmt in ast.walk(node):
277
+ if isinstance(stmt, ast.Break):
278
+ return False
279
+ return True
280
+ return False
281
+
282
+ def _visit_try(self, node: ast.Try, cfg: FunctionCFG, node_id: int):
283
+ self.try_stack.append(node_id)
284
+
285
+ try_start = self.new_node(node, "try_start")
286
+ cfg.nodes.append(try_start)
287
+ self.add_edge(node_id, try_start.id, cfg)
288
+
289
+ prev = try_start
290
+ for stmt in node.body:
291
+ stmt_node = self.new_node(stmt, type(stmt).__name__)
292
+ cfg.nodes.append(stmt_node)
293
+ self.add_edge(prev.id, stmt_node.id, cfg)
294
+ self._visit_statement(stmt, cfg, stmt_node.id)
295
+ prev = stmt_node
296
+
297
+ for handler in node.handlers:
298
+ handler_start = self.new_node(handler, "except_handler")
299
+ cfg.nodes.append(handler_start)
300
+ self.add_edge(try_start.id, handler_start.id, cfg)
301
+
302
+ handler_prev = handler_start
303
+ for stmt in handler.body:
304
+ stmt_node = self.new_node(stmt, type(stmt).__name__)
305
+ cfg.nodes.append(stmt_node)
306
+ self.add_edge(handler_prev.id, stmt_node.id, cfg)
307
+ self._visit_statement(stmt, cfg, stmt_node.id)
308
+ handler_prev = stmt_node
309
+
310
+ self.add_edge(handler_prev.id, prev.id, cfg)
311
+
312
+ if node.orelse:
313
+ else_start = self.new_node(node, "try_else")
314
+ cfg.nodes.append(else_start)
315
+ self.add_edge(prev.id, else_start.id, cfg)
316
+
317
+ else_prev = else_start
318
+ for stmt in node.orelse:
319
+ stmt_node = self.new_node(stmt, type(stmt).__name__)
320
+ cfg.nodes.append(stmt_node)
321
+ self.add_edge(else_prev.id, stmt_node.id, cfg)
322
+ self._visit_statement(stmt, cfg, stmt_node.id)
323
+ else_prev = stmt_node
324
+
325
+ self.add_edge(else_prev.id, prev.id, cfg)
326
+
327
+ if node.finalbody:
328
+ finally_start = self.new_node(node, "try_finally")
329
+ cfg.nodes.append(finally_start)
330
+ self.add_edge(prev.id, finally_start.id, cfg)
331
+
332
+ finally_prev = finally_start
333
+ for stmt in node.finalbody:
334
+ stmt_node = self.new_node(stmt, type(stmt).__name__)
335
+ cfg.nodes.append(stmt_node)
336
+ self.add_edge(finally_prev.id, stmt_node.id, cfg)
337
+ self._visit_statement(stmt, cfg, stmt_node.id)
338
+ finally_prev = stmt_node
339
+
340
+ self.add_edge(finally_prev.id, prev.id, cfg)
341
+
342
+ self.try_stack.pop()
343
+
344
+ def _visit_with(self, node: ast.With, cfg: FunctionCFG, node_id: int):
345
+ with_start = self.new_node(node, "with_start")
346
+ cfg.nodes.append(with_start)
347
+ self.add_edge(node_id, with_start.id, cfg)
348
+
349
+ prev = with_start
350
+ for stmt in node.body:
351
+ stmt_node = self.new_node(stmt, type(stmt).__name__)
352
+ cfg.nodes.append(stmt_node)
353
+ self.add_edge(prev.id, stmt_node.id, cfg)
354
+ self._visit_statement(stmt, cfg, stmt_node.id)
355
+ prev = stmt_node
356
+
357
+ with_end = self.new_node(node, "with_end")
358
+ cfg.nodes.append(with_end)
359
+ self.add_edge(prev.id, with_end.id, cfg)
360
+
361
+ def _analyze_function(self, cfg: FunctionCFG):
362
+ self._check_unreachable_code(cfg)
363
+ self._check_missing_return(cfg)
364
+ self._check_high_complexity(cfg)
365
+
366
+ def _check_unreachable_code(self, cfg: FunctionCFG):
367
+ reachable = set()
368
+
369
+ def dfs(node_id: int):
370
+ if node_id in reachable:
371
+ return
372
+ reachable.add(node_id)
373
+ for succ in cfg.nodes[node_id].successors:
374
+ dfs(succ)
375
+
376
+ dfs(cfg.entry)
377
+
378
+ for node in cfg.nodes:
379
+ if node.id not in reachable and node.node_type not in ("exit", "entry"):
380
+ self.findings.append(Finding(
381
+ engine="cfg",
382
+ severity="low",
383
+ title="Unreachable code",
384
+ description=f"Code at line {node.lineno} is unreachable",
385
+ location=f"{self.filename}:{node.lineno}",
386
+ metadata={
387
+ "snippet": self.get_snippet(node.ast_node),
388
+ "cwe": "CWE-561",
389
+ "confidence": 0.85,
390
+ "check": "unreachable_code",
391
+ }
392
+ ))
393
+
394
+ def _check_missing_return(self, cfg: FunctionCFG):
395
+ if cfg.is_generator or cfg.is_async:
396
+ return
397
+
398
+ if cfg.name.startswith("__init__") or cfg.name.startswith("__new__"):
399
+ return
400
+
401
+ has_explicit_return = any(
402
+ isinstance(n.ast_node, ast.Return) and n.ast_node.value is not None
403
+ for n in cfg.nodes
404
+ )
405
+
406
+ if not has_explicit_return and cfg.has_return:
407
+ for exit_id in cfg.exits:
408
+ pred_nodes = [cfg.nodes[p] for p in cfg.nodes[exit_id].predecessors]
409
+ for pred in pred_nodes:
410
+ if not isinstance(pred.ast_node, ast.Return):
411
+ self.findings.append(Finding(
412
+ engine="cfg",
413
+ severity="low",
414
+ title="Missing return statement",
415
+ description=f"Function {cfg.name} has paths without explicit return value",
416
+ location=f"{self.filename}:{pred.lineno}",
417
+ metadata={
418
+ "snippet": self.get_snippet(pred.ast_node),
419
+ "cwe": "CWE-393",
420
+ "confidence": 0.7,
421
+ "check": "missing_return",
422
+ "function": cfg.name,
423
+ }
424
+ ))
425
+
426
+ def _check_high_complexity(self, cfg: FunctionCFG):
427
+ if cfg.complexity > 20:
428
+ self.findings.append(Finding(
429
+ engine="cfg",
430
+ severity="medium",
431
+ title="High cyclomatic complexity",
432
+ description=f"Function {cfg.name} has cyclomatic complexity of {cfg.complexity} (threshold: 20)",
433
+ location=f"{self.filename}:{cfg.nodes[0].lineno if cfg.nodes else 0}",
434
+ metadata={
435
+ "snippet": self.get_snippet(cfg.nodes[0].ast_node) if cfg.nodes else "",
436
+ "cwe": "CWE-1121",
437
+ "confidence": 0.9,
438
+ "check": "high_complexity",
439
+ "complexity": cfg.complexity,
440
+ "function": cfg.name,
441
+ }
442
+ ))
443
+
444
+ def generic_visit(self, node: ast.AST):
445
+ if self.current_function is None:
446
+ super().generic_visit(node)
447
+
448
+
449
+ class Engine:
450
+ name = "cfg"
451
+ version = "1.0.0"
452
+
453
+ async def check_availability(self) -> bool:
454
+ return True
455
+
456
+ def resource_limits(self) -> ResourceLimits:
457
+ return ResourceLimits(max_cpu_seconds=60, max_memory_mb=512, max_files=50000)
458
+
459
+ async def run(self, ctx, config: EngineConfig) -> EngineResult:
460
+ # Handle both single IR and list of IRs
461
+ if not isinstance(ctx, list):
462
+ ctx = [ctx]
463
+ all_findings = []
464
+ errors = []
465
+ files_processed = 0
466
+ total_functions = 0
467
+ total_complexity = 0
468
+
469
+ exclude_patterns = config.toggles.get("exclude", []) if config.toggles else []
470
+
471
+ for ir in ctx:
472
+ try:
473
+ if not ir.metadata.get("language") == "python":
474
+ continue
475
+
476
+ rel_path = ir.metadata.get("path", "unknown.py")
477
+ if any(pattern in rel_path for pattern in exclude_patterns):
478
+ continue
479
+
480
+ content = ir.content.decode("utf-8", errors="replace")
481
+ source_lines = content.splitlines()
482
+ tree = ast.parse(content, filename=rel_path)
483
+
484
+ builder = CFGBuilder(rel_path, source_lines)
485
+ builder.visit(tree)
486
+
487
+ all_findings.extend(builder.findings)
488
+ total_functions += len(builder.function_cfgs)
489
+ total_complexity += sum(f.complexity for f in builder.function_cfgs.values())
490
+ files_processed += 1
491
+
492
+ except SyntaxError as e:
493
+ errors.append(f"Syntax error in {ir.metadata.get('path', 'unknown')}: {e}")
494
+ except Exception as e:
495
+ errors.append(f"Error processing {ir.metadata.get('path', 'unknown')}: {e}")
496
+
497
+ return EngineResult(
498
+ engine=self.name,
499
+ findings=all_findings,
500
+ errors=errors,
501
+ metrics={
502
+ "files_processed": files_processed,
503
+ "functions_analyzed": total_functions,
504
+ "total_complexity": total_complexity,
505
+ "avg_complexity": round(total_complexity / total_functions, 2) if total_functions > 0 else 0,
506
+ "findings_count": len(all_findings),
507
+ },
508
+ ok=len(errors) == 0
509
+ )