@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,463 @@
1
+ """Symbol table analyzer for detecting shadowed builtins, undefined names, unused imports, and late binding closures."""
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
+ import builtins
12
+
13
+ BUILTINS = set(dir(builtins)) | {
14
+ "__file__", "__name__", "__doc__", "__package__", "__loader__",
15
+ "__spec__", "__annotations__", "__builtins__", "__all__", "__path__",
16
+ "__class__", "self", "cls", "True", "False", "None", "Ellipsis", "NotImplemented"
17
+ }
18
+
19
+
20
+ @dataclass
21
+ class Scope:
22
+ name: str
23
+ parent: Optional['Scope'] = None
24
+ variables: Dict[str, 'Symbol'] = field(default_factory=dict)
25
+ imports: Dict[str, str] = field(default_factory=dict)
26
+ used_names: Set[str] = field(default_factory=set)
27
+ defined_names: Set[str] = field(default_factory=set)
28
+ is_function: bool = False
29
+ is_class: bool = False
30
+ is_module: bool = False
31
+
32
+
33
+ @dataclass
34
+ class Symbol:
35
+ name: str
36
+ scope: Scope
37
+ node: ast.AST
38
+ is_import: bool = False
39
+ is_parameter: bool = False
40
+ is_function: bool = False
41
+ is_class: bool = False
42
+ is_global: bool = False
43
+ is_nonlocal: bool = False
44
+ is_used: bool = False
45
+ is_assigned: bool = False
46
+ line: int = 0
47
+
48
+
49
+ class SymbolVisitor(ast.NodeVisitor):
50
+ def __init__(self, filename: str, source_lines: List[str]):
51
+ self.filename = filename
52
+ self.source_lines = source_lines
53
+ self.findings: List[Finding] = []
54
+ self.current_scope: Optional[Scope] = None
55
+ self.module_scope: Optional[Scope] = None
56
+ self.symbols: Dict[int, Symbol] = {}
57
+ self.closure_vars: Dict[str, List[Tuple[str, int]]] = defaultdict(list)
58
+ self.late_binding_vars: List[Tuple[str, int]] = []
59
+
60
+ def get_snippet(self, node: ast.AST, context: int = 2) -> str:
61
+ start = max(0, node.lineno - 1 - context)
62
+ end = min(len(self.source_lines),
63
+ getattr(node, 'end_lineno', node.lineno) + context if hasattr(node, 'end_lineno') else node.lineno + context)
64
+ return "\n".join(self.source_lines[start:end])
65
+
66
+ def enter_scope(self, name: str, is_function: bool = False, is_class: bool = False, is_module: bool = False):
67
+ new_scope = Scope(
68
+ name=name,
69
+ parent=self.current_scope,
70
+ is_function=is_function,
71
+ is_class=is_class,
72
+ is_module=is_module
73
+ )
74
+ if self.current_scope is None:
75
+ self.module_scope = new_scope
76
+ self.current_scope = new_scope
77
+
78
+ def exit_scope(self):
79
+ if self.current_scope and self.current_scope.parent:
80
+ self.current_scope = self.current_scope.parent
81
+ else:
82
+ self.current_scope = None
83
+
84
+ def define(self, name: str, node: ast.AST, is_import: bool = False, is_parameter: bool = False,
85
+ is_function: bool = False, is_class: bool = False, is_global: bool = False,
86
+ is_nonlocal: bool = False):
87
+ if not self.current_scope:
88
+ return
89
+
90
+ symbol = Symbol(
91
+ name=name,
92
+ scope=self.current_scope,
93
+ node=node,
94
+ is_import=is_import,
95
+ is_parameter=is_parameter,
96
+ is_function=is_function,
97
+ is_class=is_class,
98
+ is_global=is_global,
99
+ is_nonlocal=is_nonlocal,
100
+ line=node.lineno
101
+ )
102
+ self.current_scope.variables[name] = symbol
103
+ self.current_scope.defined_names.add(name)
104
+ self.symbols[id(node)] = symbol
105
+
106
+ def use(self, name: str, node: ast.AST):
107
+ if not self.current_scope:
108
+ return
109
+
110
+ scope = self.current_scope
111
+ while scope:
112
+ if name in scope.variables:
113
+ scope.variables[name].is_used = True
114
+ scope.used_names.add(name)
115
+ return
116
+ scope = scope.parent
117
+
118
+ if name not in BUILTINS and not self._is_future_import(name):
119
+ self.findings.append(Finding(
120
+ engine="symbol",
121
+ severity="medium",
122
+ title="Undefined name",
123
+ description=f"Name '{name}' is used but not defined",
124
+ location=f"{self.filename}:{node.lineno}",
125
+ metadata={
126
+ "snippet": self.get_snippet(node),
127
+ "cwe": "CWE-476",
128
+ "confidence": 0.85,
129
+ "check": "undefined_name",
130
+ "name": name,
131
+ }
132
+ ))
133
+
134
+ def _is_future_import(self, name: str) -> bool:
135
+ if self.module_scope:
136
+ return name in self.module_scope.imports
137
+ return False
138
+
139
+ def visit_Module(self, node: ast.Module):
140
+ self.enter_scope("<module>", is_module=True)
141
+ self.generic_visit(node)
142
+ self._check_unused_imports()
143
+ self._check_shadowed_builtins()
144
+ self.exit_scope()
145
+
146
+ def visit_Import(self, node: ast.Import):
147
+ for alias in node.names:
148
+ name = alias.asname or alias.name
149
+ self.define(name, node, is_import=True)
150
+ if self.current_scope:
151
+ self.current_scope.imports[name] = alias.name
152
+ self.generic_visit(node)
153
+
154
+ def visit_ImportFrom(self, node: ast.ImportFrom):
155
+ if node.module:
156
+ for alias in node.names:
157
+ name = alias.asname or alias.name
158
+ self.define(name, node, is_import=True)
159
+ if self.current_scope:
160
+ self.current_scope.imports[name] = f"{node.module}.{alias.name}"
161
+ self.generic_visit(node)
162
+
163
+ def visit_FunctionDef(self, node: ast.FunctionDef):
164
+ name = node.name
165
+ if self.current_scope and self.current_scope.is_class:
166
+ qual_name = f"{self.current_scope.name}.{name}"
167
+ else:
168
+ qual_name = name
169
+
170
+ self.define(name, node, is_function=True)
171
+
172
+ self.enter_scope(qual_name, is_function=True)
173
+
174
+ for arg in node.args.args:
175
+ self.define(arg.arg, arg, is_parameter=True)
176
+
177
+ if node.args.vararg:
178
+ self.define(node.args.vararg.arg, node.args.vararg, is_parameter=True)
179
+ if node.args.kwarg:
180
+ self.define(node.args.kwarg.arg, node.args.kwarg, is_parameter=True)
181
+
182
+ for default in node.args.defaults:
183
+ self._check_late_binding(default, node)
184
+
185
+ self.generic_visit(node)
186
+
187
+ self._check_unused_params()
188
+ self.exit_scope()
189
+
190
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
191
+ self.visit_FunctionDef(node)
192
+
193
+ def _check_late_binding(self, default: ast.AST, func_node: ast.FunctionDef):
194
+ if isinstance(default, (ast.List, ast.Dict, ast.Set, ast.Call)):
195
+ for name_node in ast.walk(default):
196
+ if isinstance(name_node, ast.Name) and isinstance(name_node.ctx, ast.Load):
197
+ self.late_binding_vars.append((name_node.id, default.lineno))
198
+
199
+ def visit_ClassDef(self, node: ast.ClassDef):
200
+ name = node.name
201
+ self.define(name, node, is_class=True)
202
+
203
+ self.enter_scope(name, is_class=True)
204
+ self.generic_visit(node)
205
+ self.exit_scope()
206
+
207
+ def visit_Assign(self, node: ast.Assign):
208
+ for target in node.targets:
209
+ self._handle_assignment_target(target, node)
210
+ self.generic_visit(node)
211
+
212
+ def visit_AnnAssign(self, node: ast.AnnAssign):
213
+ if node.target:
214
+ self._handle_assignment_target(node.target, node)
215
+ if node.value:
216
+ self.generic_visit(node.value)
217
+
218
+ def visit_AugAssign(self, node: ast.AugAssign):
219
+ self._handle_assignment_target(node.target, node)
220
+ self.generic_visit(node)
221
+
222
+ def _handle_assignment_target(self, target: ast.AST, value_node: ast.AST):
223
+ if isinstance(target, ast.Name):
224
+ name = target.id
225
+ scope = self.current_scope
226
+ while scope:
227
+ if name in scope.variables:
228
+ sym = scope.variables[name]
229
+ sym.is_assigned = True
230
+ if sym.is_parameter and not scope.is_function:
231
+ self.findings.append(Finding(
232
+ engine="symbol",
233
+ severity="low",
234
+ title="Parameter reassignment",
235
+ description=f"Function parameter '{name}' is reassigned",
236
+ location=f"{self.filename}:{target.lineno}",
237
+ metadata={
238
+ "snippet": self.get_snippet(target),
239
+ "cwe": "CWE-563",
240
+ "confidence": 0.75,
241
+ "check": "param_reassignment",
242
+ "name": name,
243
+ }
244
+ ))
245
+ break
246
+ scope = scope.parent
247
+ else:
248
+ self.define(name, target)
249
+ elif isinstance(target, ast.Attribute):
250
+ pass
251
+ elif isinstance(target, ast.Subscript):
252
+ pass
253
+ elif isinstance(target, (ast.Tuple, ast.List)):
254
+ for elt in target.elts:
255
+ self._handle_assignment_target(elt, value_node)
256
+
257
+ def visit_Global(self, node: ast.Global):
258
+ for name in node.names:
259
+ if self.module_scope and name in self.module_scope.variables:
260
+ self.module_scope.variables[name].is_global = True
261
+ if self.current_scope:
262
+ self.current_scope.variables[name] = Symbol(
263
+ name=name, scope=self.module_scope or self.current_scope,
264
+ node=node, is_global=True, line=node.lineno
265
+ )
266
+
267
+ def visit_Nonlocal(self, node: ast.Nonlocal):
268
+ for name in node.names:
269
+ scope = self.current_scope.parent if self.current_scope else None
270
+ while scope and not scope.is_function:
271
+ scope = scope.parent
272
+ if scope and name in scope.variables:
273
+ scope.variables[name].is_nonlocal = True
274
+
275
+ def visit_Name(self, node: ast.Name):
276
+ if isinstance(node.ctx, ast.Load):
277
+ self.use(node.id, node)
278
+ elif isinstance(node.ctx, ast.Store):
279
+ pass
280
+ elif isinstance(node.ctx, ast.Del):
281
+ pass
282
+ self.generic_visit(node)
283
+
284
+ def visit_Lambda(self, node: ast.Lambda):
285
+ self.enter_scope("<lambda>", is_function=True)
286
+ for arg in node.args.args:
287
+ self.define(arg.arg, arg, is_parameter=True)
288
+ self.generic_visit(node)
289
+ self.exit_scope()
290
+
291
+ def visit_Comprehension(self, node: ast.comprehension):
292
+ self.define(node.target.id if isinstance(node.target, ast.Name) else "_", node, is_parameter=True)
293
+ for if_clause in node.ifs:
294
+ self.generic_visit(if_clause)
295
+ self.generic_visit(node.iter)
296
+
297
+ def visit_GeneratorExp(self, node: ast.GeneratorExp):
298
+ self.enter_scope("<genexpr>", is_function=True)
299
+ for gen in node.generators:
300
+ self.visit_Comprehension(gen)
301
+ self.generic_visit(node.elt)
302
+ self.exit_scope()
303
+
304
+ def visit_ListComp(self, node: ast.ListComp):
305
+ self.visit_GeneratorExp(node)
306
+
307
+ def visit_SetComp(self, node: ast.SetComp):
308
+ self.visit_GeneratorExp(node)
309
+
310
+ def visit_DictComp(self, node: ast.DictComp):
311
+ self.enter_scope("<dictexpr>", is_function=True)
312
+ for gen in node.generators:
313
+ self.visit_Comprehension(gen)
314
+ self.generic_visit(node.key)
315
+ self.generic_visit(node.value)
316
+ self.exit_scope()
317
+
318
+ def _check_unused_params(self):
319
+ if not self.current_scope or not self.current_scope.is_function:
320
+ return
321
+ for name, sym in self.current_scope.variables.items():
322
+ if sym.is_parameter and not sym.is_used and name != "_":
323
+ self.findings.append(Finding(
324
+ engine="symbol",
325
+ severity="low",
326
+ title="Unused function parameter",
327
+ description=f"Parameter '{name}' is defined but never used",
328
+ location=f"{self.filename}:{sym.line}",
329
+ metadata={
330
+ "snippet": self.get_snippet(sym.node),
331
+ "cwe": "CWE-563",
332
+ "confidence": 0.8,
333
+ "check": "unused_parameter",
334
+ "name": name,
335
+ }
336
+ ))
337
+
338
+ def _check_unused_imports(self):
339
+ if not self.module_scope:
340
+ return
341
+ for name, sym in self.module_scope.variables.items():
342
+ if sym.is_import and not sym.is_used:
343
+ if name.startswith("_"):
344
+ continue
345
+ if self.module_scope.imports.get(name, "").startswith("__future__"):
346
+ continue
347
+ self.findings.append(Finding(
348
+ engine="symbol",
349
+ severity="low",
350
+ title="Unused import",
351
+ description=f"Import '{name}' is never used",
352
+ location=f"{self.filename}:{sym.line}",
353
+ metadata={
354
+ "snippet": self.get_snippet(sym.node),
355
+ "cwe": "CWE-1041",
356
+ "confidence": 0.85,
357
+ "check": "unused_import",
358
+ "imported": name,
359
+ "module": self.module_scope.imports.get(name, ""),
360
+ }
361
+ ))
362
+
363
+ def _check_shadowed_builtins(self):
364
+ if not self.module_scope:
365
+ return
366
+ for name in self.module_scope.variables:
367
+ if name in BUILTINS and not name.startswith("_"):
368
+ sym = self.module_scope.variables[name]
369
+ if sym.is_import or sym.is_function or sym.is_class:
370
+ self.findings.append(Finding(
371
+ engine="symbol",
372
+ severity="medium",
373
+ title="Shadowed builtin",
374
+ description=f"Builtin '{name}' is shadowed by local definition",
375
+ location=f"{self.filename}:{sym.line}",
376
+ metadata={
377
+ "snippet": self.get_snippet(sym.node),
378
+ "cwe": "CWE-1228",
379
+ "confidence": 0.9,
380
+ "check": "shadowed_builtin",
381
+ "name": name,
382
+ }
383
+ ))
384
+
385
+ for scope_name, scope in self._collect_scopes(self.module_scope):
386
+ for name, sym in scope.variables.items():
387
+ if name in BUILTINS and not name.startswith("_"):
388
+ if sym.is_parameter:
389
+ self.findings.append(Finding(
390
+ engine="symbol",
391
+ severity="medium",
392
+ title="Shadowed builtin parameter",
393
+ description=f"Function parameter '{name}' shadows builtin",
394
+ location=f"{self.filename}:{sym.line}",
395
+ metadata={
396
+ "snippet": self.get_snippet(sym.node),
397
+ "cwe": "CWE-1228",
398
+ "confidence": 0.85,
399
+ "check": "shadowed_builtin_param",
400
+ "name": name,
401
+ "function": scope.name,
402
+ }
403
+ ))
404
+
405
+ def _collect_scopes(self, scope: Scope) -> List[Tuple[str, Scope]]:
406
+ result = [(scope.name, scope)]
407
+ for var in scope.variables.values():
408
+ if var.is_function or var.is_class:
409
+ pass
410
+ return result
411
+
412
+
413
+ class Engine:
414
+ name = "symbol"
415
+ version = "1.0.0"
416
+
417
+ async def check_availability(self) -> bool:
418
+ return True
419
+
420
+ def resource_limits(self) -> ResourceLimits:
421
+ return ResourceLimits(max_cpu_seconds=60, max_memory_mb=512, max_files=50000)
422
+
423
+ async def run(self, ctx, config: EngineConfig) -> EngineResult:
424
+ # Handle both single IR and list of IRs
425
+ if not isinstance(ctx, list):
426
+ ctx = [ctx]
427
+ all_findings = []
428
+ errors = []
429
+ files_processed = 0
430
+
431
+ exclude_patterns = config.toggles.get("exclude", []) if config.toggles else []
432
+
433
+ for ir in ctx:
434
+ try:
435
+ if not ir.metadata.get("language") == "python":
436
+ continue
437
+
438
+ rel_path = ir.metadata.get("path", "unknown.py")
439
+ if any(pattern in rel_path for pattern in exclude_patterns):
440
+ continue
441
+
442
+ content = ir.content.decode("utf-8", errors="replace")
443
+ source_lines = content.splitlines()
444
+ tree = ast.parse(content, filename=rel_path)
445
+
446
+ visitor = SymbolVisitor(rel_path, source_lines)
447
+ visitor.visit(tree)
448
+
449
+ all_findings.extend(visitor.findings)
450
+ files_processed += 1
451
+
452
+ except SyntaxError as e:
453
+ errors.append(f"Syntax error in {ir.metadata.get('path', 'unknown')}: {e}")
454
+ except Exception as e:
455
+ errors.append(f"Error processing {ir.metadata.get('path', 'unknown')}: {e}")
456
+
457
+ return EngineResult(
458
+ engine=self.name,
459
+ findings=all_findings,
460
+ errors=errors,
461
+ metrics={"files_processed": files_processed, "findings_count": len(all_findings)},
462
+ ok=len(errors) == 0
463
+ )