@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.
- package/LICENSE +21 -0
- package/README.md +248 -0
- package/assets/logo.png +0 -0
- package/bin/orion.js +48 -0
- package/docs/API_REFERENCE.md +211 -0
- package/docs/ARCHITECTURE.md +183 -0
- package/docs/CHANGELOG.md +112 -0
- package/docs/CONTRIBUTING.md +157 -0
- package/docs/ENGINE_CATALOG.md +87 -0
- package/docs/GETTING_STARTED.md +226 -0
- package/package.json +48 -0
- package/pyproject.toml +16 -0
- package/src/orion/__init__.py +0 -0
- package/src/orion/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/orion/engine/__init__.py +17 -0
- package/src/orion/engine/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/orion/engine/__pycache__/config.cpython-313.pyc +0 -0
- package/src/orion/engine/__pycache__/core.cpython-313.pyc +0 -0
- package/src/orion/engine/__pycache__/ir.cpython-313.pyc +0 -0
- package/src/orion/engine/__pycache__/languages.cpython-313.pyc +0 -0
- package/src/orion/engine/__pycache__/registry.cpython-313.pyc +0 -0
- package/src/orion/engine/__pycache__/results.cpython-313.pyc +0 -0
- package/src/orion/engine/__pycache__/runner.cpython-313.pyc +0 -0
- package/src/orion/engine/api_security/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/api_security/engine.py +125 -0
- package/src/orion/engine/architecture/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/architecture/engine.py +463 -0
- package/src/orion/engine/authorization/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/authorization/engine.py +337 -0
- package/src/orion/engine/callgraph/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/callgraph/engine.py +236 -0
- package/src/orion/engine/cfg/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/cfg/engine.py +509 -0
- package/src/orion/engine/chaos/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/chaos/engine.py +268 -0
- package/src/orion/engine/chaos_config/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/chaos_config/engine.py +322 -0
- package/src/orion/engine/complexity/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/complexity/engine.py +144 -0
- package/src/orion/engine/compliance/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/compliance/engine.py +325 -0
- package/src/orion/engine/config.py +41 -0
- package/src/orion/engine/config_engine/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/config_engine/engine.py +364 -0
- package/src/orion/engine/config_validation/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/config_validation/engine.py +313 -0
- package/src/orion/engine/container/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/container/engine.py +546 -0
- package/src/orion/engine/contract_probe/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/contract_probe/engine.py +316 -0
- package/src/orion/engine/core.py +75 -0
- package/src/orion/engine/crypto_guard/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/crypto_guard/engine.py +134 -0
- package/src/orion/engine/data_residency/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/data_residency/engine.py +238 -0
- package/src/orion/engine/dataflow/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/dataflow/engine.py +398 -0
- package/src/orion/engine/dead_import/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/dead_import/engine.py +293 -0
- package/src/orion/engine/deps/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/deps/engine.py +244 -0
- package/src/orion/engine/dispatcher.py +350 -0
- package/src/orion/engine/docs/AUDIT_PROMPT.md +1248 -0
- package/src/orion/engine/duplication/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/duplication/engine.py +120 -0
- package/src/orion/engine/error_handling/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/error_handling/engine.py +256 -0
- package/src/orion/engine/fp_learn/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/fp_learn/engine.py +248 -0
- package/src/orion/engine/hunter/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/hunter/engine.py +324 -0
- package/src/orion/engine/iac_security/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/iac_security/engine.py +218 -0
- package/src/orion/engine/ir.py +22 -0
- package/src/orion/engine/languages.py +454 -0
- package/src/orion/engine/license/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/license/engine.py +172 -0
- package/src/orion/engine/lint/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/lint/engine.py +558 -0
- package/src/orion/engine/llm_security/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/llm_security/engine.py +125 -0
- package/src/orion/engine/logging_c/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/logging_c/engine.py +164 -0
- package/src/orion/engine/noise/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/noise/engine.py +140 -0
- package/src/orion/engine/orphan/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/orphan/engine.py +244 -0
- package/src/orion/engine/perf_guard/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/perf_guard/engine.py +345 -0
- package/src/orion/engine/prompt_security/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/prompt_security/detectors/__init__.py +4 -0
- package/src/orion/engine/prompt_security/engine.py +170 -0
- package/src/orion/engine/provenance/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/provenance/engine.py +242 -0
- package/src/orion/engine/registry.py +55 -0
- package/src/orion/engine/regression/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/regression/engine.py +213 -0
- package/src/orion/engine/results.py +21 -0
- package/src/orion/engine/runner.py +1310 -0
- package/src/orion/engine/sandbox_escape/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/sandbox_escape/engine.py +284 -0
- package/src/orion/engine/sast/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/sast/engine.py +217 -0
- package/src/orion/engine/sast/patterns/__init__.py +513 -0
- package/src/orion/engine/sast/patterns/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/orion/engine/secrets/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/secrets/engine.py +77 -0
- package/src/orion/engine/secrets/patterns/__init__.py +74 -0
- package/src/orion/engine/secrets/patterns/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/orion/engine/sensitive_data/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/sensitive_data/engine.py +143 -0
- package/src/orion/engine/state/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/state/engine.py +218 -0
- package/src/orion/engine/supply_chain_sbom/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/supply_chain_sbom/engine.py +290 -0
- package/src/orion/engine/symbol/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/symbol/engine.py +463 -0
- package/src/orion/engine/taint/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/taint/engine.py +464 -0
- package/src/orion/engine/test_coverage/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/test_coverage/engine.py +445 -0
- package/src/orion/engine/threat_model/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/threat_model/engine.py +126 -0
- package/src/orion/engine/wiring_trace/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/wiring_trace/engine.py +200 -0
- package/src/orion_engine.egg-info/PKG-INFO +7 -0
- package/src/orion_engine.egg-info/SOURCES.txt +65 -0
- package/src/orion_engine.egg-info/dependency_links.txt +1 -0
- package/src/orion_engine.egg-info/entry_points.txt +2 -0
- 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
|
+
)
|
|
Binary file
|