@orionscanner/cli 0.1.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -190
- package/bin/orion.js +30 -29
- package/dist/orion.exe +0 -0
- package/package.json +46 -48
- package/docs/API_REFERENCE.md +0 -211
- package/docs/ARCHITECTURE.md +0 -183
- package/docs/CHANGELOG.md +0 -112
- package/docs/CONTRIBUTING.md +0 -157
- package/docs/ENGINE_CATALOG.md +0 -87
- package/docs/GETTING_STARTED.md +0 -226
- package/pyproject.toml +0 -16
- package/src/orion/__init__.py +0 -0
- package/src/orion/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/orion/engine/__init__.py +0 -17
- 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 +0 -125
- package/src/orion/engine/architecture/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/architecture/engine.py +0 -463
- package/src/orion/engine/authorization/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/authorization/engine.py +0 -337
- package/src/orion/engine/callgraph/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/callgraph/engine.py +0 -236
- package/src/orion/engine/cfg/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/cfg/engine.py +0 -509
- package/src/orion/engine/chaos/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/chaos/engine.py +0 -268
- package/src/orion/engine/chaos_config/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/chaos_config/engine.py +0 -322
- package/src/orion/engine/complexity/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/complexity/engine.py +0 -144
- package/src/orion/engine/compliance/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/compliance/engine.py +0 -325
- package/src/orion/engine/config.py +0 -41
- package/src/orion/engine/config_engine/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/config_engine/engine.py +0 -364
- package/src/orion/engine/config_validation/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/config_validation/engine.py +0 -313
- package/src/orion/engine/container/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/container/engine.py +0 -546
- package/src/orion/engine/contract_probe/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/contract_probe/engine.py +0 -316
- package/src/orion/engine/core.py +0 -75
- package/src/orion/engine/crypto_guard/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/crypto_guard/engine.py +0 -134
- package/src/orion/engine/data_residency/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/data_residency/engine.py +0 -238
- package/src/orion/engine/dataflow/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/dataflow/engine.py +0 -398
- package/src/orion/engine/dead_import/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/dead_import/engine.py +0 -293
- package/src/orion/engine/deps/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/deps/engine.py +0 -244
- package/src/orion/engine/dispatcher.py +0 -350
- package/src/orion/engine/docs/AUDIT_PROMPT.md +0 -1248
- package/src/orion/engine/duplication/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/duplication/engine.py +0 -120
- package/src/orion/engine/error_handling/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/error_handling/engine.py +0 -256
- package/src/orion/engine/fp_learn/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/fp_learn/engine.py +0 -248
- package/src/orion/engine/hunter/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/hunter/engine.py +0 -324
- package/src/orion/engine/iac_security/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/iac_security/engine.py +0 -218
- package/src/orion/engine/ir.py +0 -22
- package/src/orion/engine/languages.py +0 -454
- package/src/orion/engine/license/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/license/engine.py +0 -172
- package/src/orion/engine/lint/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/lint/engine.py +0 -558
- package/src/orion/engine/llm_security/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/llm_security/engine.py +0 -125
- package/src/orion/engine/logging_c/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/logging_c/engine.py +0 -164
- package/src/orion/engine/noise/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/noise/engine.py +0 -140
- package/src/orion/engine/orphan/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/orphan/engine.py +0 -244
- package/src/orion/engine/perf_guard/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/perf_guard/engine.py +0 -345
- package/src/orion/engine/prompt_security/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/prompt_security/detectors/__init__.py +0 -4
- package/src/orion/engine/prompt_security/engine.py +0 -170
- package/src/orion/engine/provenance/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/provenance/engine.py +0 -242
- package/src/orion/engine/registry.py +0 -55
- package/src/orion/engine/regression/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/regression/engine.py +0 -213
- package/src/orion/engine/results.py +0 -21
- package/src/orion/engine/runner.py +0 -1310
- package/src/orion/engine/sandbox_escape/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/sandbox_escape/engine.py +0 -284
- package/src/orion/engine/sast/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/sast/engine.py +0 -217
- package/src/orion/engine/sast/patterns/__init__.py +0 -513
- 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 +0 -77
- package/src/orion/engine/secrets/patterns/__init__.py +0 -74
- 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 +0 -143
- package/src/orion/engine/state/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/state/engine.py +0 -218
- package/src/orion/engine/supply_chain_sbom/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/supply_chain_sbom/engine.py +0 -290
- package/src/orion/engine/symbol/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/symbol/engine.py +0 -463
- package/src/orion/engine/taint/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/taint/engine.py +0 -464
- package/src/orion/engine/test_coverage/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/test_coverage/engine.py +0 -445
- package/src/orion/engine/threat_model/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/threat_model/engine.py +0 -126
- package/src/orion/engine/wiring_trace/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/wiring_trace/engine.py +0 -200
- package/src/orion_engine.egg-info/PKG-INFO +0 -7
- package/src/orion_engine.egg-info/SOURCES.txt +0 -65
- package/src/orion_engine.egg-info/dependency_links.txt +0 -1
- package/src/orion_engine.egg-info/entry_points.txt +0 -2
- package/src/orion_engine.egg-info/top_level.txt +0 -1
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
"""Dead import detection engine - finds unused imports using AST analysis."""
|
|
2
|
-
import ast
|
|
3
|
-
from typing import Dict, List, Set, Optional, Any
|
|
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 ImportInfo:
|
|
13
|
-
name: str
|
|
14
|
-
alias: Optional[str]
|
|
15
|
-
module: str
|
|
16
|
-
lineno: int
|
|
17
|
-
is_from_import: bool
|
|
18
|
-
is_future: bool = False
|
|
19
|
-
is_side_effect: bool = False
|
|
20
|
-
used: bool = False
|
|
21
|
-
used_as: Set[str] = field(default_factory=set)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class ImportVisitor(ast.NodeVisitor):
|
|
25
|
-
def __init__(self, filename: str, source_lines: List[str]):
|
|
26
|
-
self.filename = filename
|
|
27
|
-
self.source_lines = source_lines
|
|
28
|
-
self.findings: List[Finding] = []
|
|
29
|
-
self.imports: List[ImportInfo] = []
|
|
30
|
-
self.used_names: Set[str] = set()
|
|
31
|
-
self.used_attributes: Dict[str, Set[str]] = defaultdict(set)
|
|
32
|
-
self.scope_stack: List[Set[str]] = [set()]
|
|
33
|
-
self.future_imports: Set[str] = set()
|
|
34
|
-
self.current_class: Optional[str] = None
|
|
35
|
-
|
|
36
|
-
def get_snippet(self, node: ast.AST, context: int = 2) -> str:
|
|
37
|
-
start = max(0, node.lineno - 1 - context)
|
|
38
|
-
end = min(len(self.source_lines),
|
|
39
|
-
getattr(node, 'end_lineno', node.lineno) + context if hasattr(node, 'end_lineno') else node.lineno + context)
|
|
40
|
-
return "\n".join(self.source_lines[start:end])
|
|
41
|
-
|
|
42
|
-
def current_scope(self) -> Set[str]:
|
|
43
|
-
return self.scope_stack[-1]
|
|
44
|
-
|
|
45
|
-
def enter_scope(self):
|
|
46
|
-
self.scope_stack.append(set())
|
|
47
|
-
|
|
48
|
-
def exit_scope(self):
|
|
49
|
-
if len(self.scope_stack) > 1:
|
|
50
|
-
self.scope_stack.pop()
|
|
51
|
-
|
|
52
|
-
def mark_used(self, name: str):
|
|
53
|
-
self.used_names.add(name)
|
|
54
|
-
for scope in self.scope_stack:
|
|
55
|
-
scope.add(name)
|
|
56
|
-
|
|
57
|
-
def visit_Import(self, node: ast.Import):
|
|
58
|
-
for alias in node.names:
|
|
59
|
-
name = alias.asname or alias.name
|
|
60
|
-
module = alias.name
|
|
61
|
-
|
|
62
|
-
is_future = module == "__future__"
|
|
63
|
-
is_side_effect = is_future or module in (
|
|
64
|
-
"typing", "typing_extensions", "types", "dataclasses",
|
|
65
|
-
"contextlib", "warnings", "logging", "sys", "os", "builtins"
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
info = ImportInfo(
|
|
69
|
-
name=name,
|
|
70
|
-
alias=alias.asname,
|
|
71
|
-
module=module,
|
|
72
|
-
lineno=node.lineno,
|
|
73
|
-
is_from_import=False,
|
|
74
|
-
is_future=is_future,
|
|
75
|
-
is_side_effect=is_side_effect,
|
|
76
|
-
)
|
|
77
|
-
self.imports.append(info)
|
|
78
|
-
|
|
79
|
-
if not is_future:
|
|
80
|
-
self.current_scope().add(name)
|
|
81
|
-
|
|
82
|
-
self.generic_visit(node)
|
|
83
|
-
|
|
84
|
-
def visit_ImportFrom(self, node: ast.ImportFrom):
|
|
85
|
-
if node.module:
|
|
86
|
-
is_future = node.module == "__future__"
|
|
87
|
-
is_init_side_effect = node.module.endswith(".__init__") or (
|
|
88
|
-
node.level > 0 and not node.module
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
for alias in node.names:
|
|
92
|
-
name = alias.asname or alias.name
|
|
93
|
-
full_module = f"{node.module}.{alias.name}" if node.module else alias.name
|
|
94
|
-
|
|
95
|
-
is_side_effect = is_future or is_init_side_effect or (
|
|
96
|
-
node.module in ("typing", "typing_extensions", "types", "dataclasses")
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
info = ImportInfo(
|
|
100
|
-
name=name,
|
|
101
|
-
alias=alias.asname,
|
|
102
|
-
module=full_module,
|
|
103
|
-
lineno=node.lineno,
|
|
104
|
-
is_from_import=True,
|
|
105
|
-
is_future=is_future,
|
|
106
|
-
is_side_effect=is_side_effect,
|
|
107
|
-
)
|
|
108
|
-
self.imports.append(info)
|
|
109
|
-
|
|
110
|
-
if not is_future:
|
|
111
|
-
self.current_scope().add(name)
|
|
112
|
-
|
|
113
|
-
self.generic_visit(node)
|
|
114
|
-
|
|
115
|
-
def visit_Name(self, node: ast.Name):
|
|
116
|
-
if isinstance(node.ctx, ast.Load):
|
|
117
|
-
self.mark_used(node.id)
|
|
118
|
-
self.generic_visit(node)
|
|
119
|
-
|
|
120
|
-
def visit_Attribute(self, node: ast.Attribute):
|
|
121
|
-
if isinstance(node.ctx, ast.Load):
|
|
122
|
-
if isinstance(node.value, ast.Name):
|
|
123
|
-
base = node.value.id
|
|
124
|
-
self.used_attributes[base].add(node.attr)
|
|
125
|
-
self.mark_used(base)
|
|
126
|
-
self.generic_visit(node)
|
|
127
|
-
|
|
128
|
-
def visit_FunctionDef(self, node: ast.FunctionDef):
|
|
129
|
-
self.enter_scope()
|
|
130
|
-
for arg in node.args.args:
|
|
131
|
-
self.current_scope().add(arg.arg)
|
|
132
|
-
if node.args.vararg:
|
|
133
|
-
self.current_scope().add(node.args.vararg.arg)
|
|
134
|
-
if node.args.kwarg:
|
|
135
|
-
self.current_scope().add(node.args.kwarg.arg)
|
|
136
|
-
self.generic_visit(node)
|
|
137
|
-
self.exit_scope()
|
|
138
|
-
|
|
139
|
-
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
|
|
140
|
-
self.visit_FunctionDef(node)
|
|
141
|
-
|
|
142
|
-
def visit_ClassDef(self, node: ast.ClassDef):
|
|
143
|
-
old_class = self.current_class
|
|
144
|
-
self.current_class = node.name
|
|
145
|
-
self.enter_scope()
|
|
146
|
-
self.generic_visit(node)
|
|
147
|
-
self.exit_scope()
|
|
148
|
-
self.current_class = old_class
|
|
149
|
-
|
|
150
|
-
def visit_Lambda(self, node: ast.Lambda):
|
|
151
|
-
self.enter_scope()
|
|
152
|
-
for arg in node.args.args:
|
|
153
|
-
self.current_scope().add(arg.arg)
|
|
154
|
-
self.generic_visit(node)
|
|
155
|
-
self.exit_scope()
|
|
156
|
-
|
|
157
|
-
def visit_Comprehension(self, node: ast.comprehension):
|
|
158
|
-
if isinstance(node.target, ast.Name):
|
|
159
|
-
self.current_scope().add(node.target.id)
|
|
160
|
-
elif isinstance(node.target, (ast.Tuple, ast.List)):
|
|
161
|
-
for elt in node.target.elts:
|
|
162
|
-
if isinstance(elt, ast.Name):
|
|
163
|
-
self.current_scope().add(elt.id)
|
|
164
|
-
self.generic_visit(node)
|
|
165
|
-
|
|
166
|
-
def visit_Global(self, node: ast.Global):
|
|
167
|
-
for name in node.names:
|
|
168
|
-
self.mark_used(name)
|
|
169
|
-
|
|
170
|
-
def visit_Nonlocal(self, node: ast.Nonlocal):
|
|
171
|
-
for name in node.names:
|
|
172
|
-
self.mark_used(name)
|
|
173
|
-
|
|
174
|
-
def visit_ExceptHandler(self, node: ast.ExceptHandler):
|
|
175
|
-
if node.name:
|
|
176
|
-
self.current_scope().add(node.name)
|
|
177
|
-
self.generic_visit(node)
|
|
178
|
-
|
|
179
|
-
def visit_With(self, node: ast.With):
|
|
180
|
-
for item in node.items:
|
|
181
|
-
if item.optional_vars:
|
|
182
|
-
if isinstance(item.optional_vars, ast.Name):
|
|
183
|
-
self.current_scope().add(item.optional_vars.id)
|
|
184
|
-
elif isinstance(item.optional_vars, (ast.Tuple, ast.List)):
|
|
185
|
-
for elt in item.optional_vars.elts:
|
|
186
|
-
if isinstance(elt, ast.Name):
|
|
187
|
-
self.current_scope().add(elt.id)
|
|
188
|
-
self.generic_visit(node)
|
|
189
|
-
|
|
190
|
-
def visit_Match(self, node: ast.Match):
|
|
191
|
-
self.enter_scope()
|
|
192
|
-
self.generic_visit(node)
|
|
193
|
-
self.exit_scope()
|
|
194
|
-
|
|
195
|
-
def visit_match_case(self, node: ast.match_case):
|
|
196
|
-
self.enter_scope()
|
|
197
|
-
self.generic_visit(node.pattern)
|
|
198
|
-
self.generic_visit(node.guard)
|
|
199
|
-
for stmt in node.body:
|
|
200
|
-
self.generic_visit(stmt)
|
|
201
|
-
self.exit_scope()
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
class Engine:
|
|
205
|
-
name = "dead_import"
|
|
206
|
-
version = "1.0.0"
|
|
207
|
-
|
|
208
|
-
async def check_availability(self) -> bool:
|
|
209
|
-
return True
|
|
210
|
-
|
|
211
|
-
def resource_limits(self) -> ResourceLimits:
|
|
212
|
-
return ResourceLimits(max_cpu_seconds=60, max_memory_mb=512, max_files=50000)
|
|
213
|
-
|
|
214
|
-
async def run(self, ctx, config: EngineConfig) -> EngineResult:
|
|
215
|
-
# Handle both single IR and list of IRs
|
|
216
|
-
if not isinstance(ctx, list):
|
|
217
|
-
ctx = [ctx]
|
|
218
|
-
all_findings = []
|
|
219
|
-
errors = []
|
|
220
|
-
files_processed = 0
|
|
221
|
-
|
|
222
|
-
exclude_patterns = config.toggles.get("exclude", []) if config.toggles else []
|
|
223
|
-
|
|
224
|
-
for ir in ctx:
|
|
225
|
-
try:
|
|
226
|
-
if not ir.metadata.get("language") == "python":
|
|
227
|
-
continue
|
|
228
|
-
|
|
229
|
-
rel_path = ir.metadata.get("path", "unknown.py")
|
|
230
|
-
if any(pattern in rel_path for pattern in exclude_patterns):
|
|
231
|
-
continue
|
|
232
|
-
|
|
233
|
-
content = ir.content.decode("utf-8", errors="replace")
|
|
234
|
-
source_lines = content.splitlines()
|
|
235
|
-
tree = ast.parse(content, filename=rel_path)
|
|
236
|
-
|
|
237
|
-
visitor = ImportVisitor(rel_path, source_lines)
|
|
238
|
-
visitor.visit(tree)
|
|
239
|
-
|
|
240
|
-
for imp in visitor.imports:
|
|
241
|
-
if imp.is_future or imp.is_side_effect:
|
|
242
|
-
continue
|
|
243
|
-
|
|
244
|
-
used_name = imp.alias or imp.name
|
|
245
|
-
|
|
246
|
-
is_used = (
|
|
247
|
-
used_name in visitor.used_names or
|
|
248
|
-
used_name in visitor.used_attributes or
|
|
249
|
-
any(used_name in scope for scope in visitor.scope_stack)
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
if not is_used:
|
|
253
|
-
for attr in visitor.used_attributes.get(used_name, []):
|
|
254
|
-
is_used = True
|
|
255
|
-
break
|
|
256
|
-
|
|
257
|
-
if not is_used:
|
|
258
|
-
all_findings.append(Finding(
|
|
259
|
-
engine="dead_import",
|
|
260
|
-
severity="low",
|
|
261
|
-
title="Unused import",
|
|
262
|
-
description=f"Import '{imp.name}' from '{imp.module}' is never used",
|
|
263
|
-
location=f"{rel_path}:{imp.lineno}",
|
|
264
|
-
metadata={
|
|
265
|
-
"snippet": visitor.get_snippet_by_line(imp.lineno),
|
|
266
|
-
"cwe": "CWE-1041",
|
|
267
|
-
"confidence": 0.9,
|
|
268
|
-
"imported": imp.name,
|
|
269
|
-
"module": imp.module,
|
|
270
|
-
"alias": imp.alias,
|
|
271
|
-
"is_from_import": imp.is_from_import,
|
|
272
|
-
}
|
|
273
|
-
))
|
|
274
|
-
|
|
275
|
-
files_processed += 1
|
|
276
|
-
|
|
277
|
-
except SyntaxError as e:
|
|
278
|
-
errors.append(f"Syntax error in {ir.metadata.get('path', 'unknown')}: {e}")
|
|
279
|
-
except Exception as e:
|
|
280
|
-
errors.append(f"Error processing {ir.metadata.get('path', 'unknown')}: {e}")
|
|
281
|
-
|
|
282
|
-
return EngineResult(
|
|
283
|
-
engine=self.name,
|
|
284
|
-
findings=all_findings,
|
|
285
|
-
errors=errors,
|
|
286
|
-
metrics={"files_processed": files_processed, "findings_count": len(all_findings)},
|
|
287
|
-
ok=len(errors) == 0
|
|
288
|
-
)
|
|
289
|
-
|
|
290
|
-
def get_snippet_by_line(self, lines: List[str], line: int, context: int = 2) -> str:
|
|
291
|
-
start = max(0, line - 1 - context)
|
|
292
|
-
end = min(len(lines), line + context)
|
|
293
|
-
return "\n".join(lines[start:end])
|
|
Binary file
|
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
"""Dependency vulnerability scanner engine."""
|
|
2
|
-
import re
|
|
3
|
-
import json
|
|
4
|
-
import tomllib
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from orion.engine.config import EngineConfig
|
|
7
|
-
from orion.engine.ir import OrionIR
|
|
8
|
-
from orion.engine.results import EngineResult, Finding
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class Engine:
|
|
12
|
-
name = "deps"
|
|
13
|
-
version = "1.0.0"
|
|
14
|
-
|
|
15
|
-
VULN_DB = {
|
|
16
|
-
"requests": [("2.28.0", "CVE-2022-32891", "2.25.1")],
|
|
17
|
-
"urllib3": [("1.26.5", "CVE-2021-33503", "1.26.4")],
|
|
18
|
-
"django": [("4.1.7", "CVE-2022-34265", "4.1.6")],
|
|
19
|
-
"flask": [("2.2.2", "CVE-2022-29242", "2.2.1")],
|
|
20
|
-
"spring-boot": [("2.7.8", "CVE-2022-22965", "2.7.7")],
|
|
21
|
-
"log4j": [("2.17.1", "CVE-2021-44228", "2.17.0")],
|
|
22
|
-
"jackson-databind": [("2.13.2", "CVE-2020-36518", "2.13.1")],
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
GPL_LICENSES = {"GPL-2.0", "GPL-3.0", "AGPL-3.0", "LGPL-2.1", "LGPL-3.0"}
|
|
26
|
-
|
|
27
|
-
async def check_availability(self) -> bool:
|
|
28
|
-
return True
|
|
29
|
-
|
|
30
|
-
def resource_limits(self):
|
|
31
|
-
from orion.engine.config import ResourceLimits
|
|
32
|
-
return ResourceLimits(max_cpu_seconds=60, max_memory_mb=256)
|
|
33
|
-
|
|
34
|
-
async def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
|
|
35
|
-
try:
|
|
36
|
-
text = ir.content.decode("utf-8", errors="replace")
|
|
37
|
-
except Exception:
|
|
38
|
-
text = str(ir.content)
|
|
39
|
-
|
|
40
|
-
path = ir.metadata.get("path", "unknown")
|
|
41
|
-
filename = Path(path).name.lower()
|
|
42
|
-
findings = []
|
|
43
|
-
deps = {}
|
|
44
|
-
|
|
45
|
-
if filename in ("requirements.txt",) or filename.startswith("requirements-") and filename.endswith(".txt"):
|
|
46
|
-
deps = self._parse_requirements(text)
|
|
47
|
-
elif filename == "pyproject.toml":
|
|
48
|
-
deps = self._parse_pyproject_toml(text)
|
|
49
|
-
elif filename == "package.json":
|
|
50
|
-
deps = self._parse_package_json(text)
|
|
51
|
-
elif filename == "pipfile":
|
|
52
|
-
deps = self._parse_pipfile(text)
|
|
53
|
-
elif filename in ("poetry.lock", "pnpm-lock.yaml", "yarn.lock"):
|
|
54
|
-
deps = self._parse_lock_file(text, filename)
|
|
55
|
-
|
|
56
|
-
if not deps:
|
|
57
|
-
return EngineResult(engine=self.name, findings=findings, ok=True)
|
|
58
|
-
|
|
59
|
-
for name, version in deps.items():
|
|
60
|
-
findings.extend(self._check_vulnerabilities(name, version, path))
|
|
61
|
-
findings.extend(self._check_license_conflicts(name, version, path))
|
|
62
|
-
findings.extend(self._check_outdated(name, version, path))
|
|
63
|
-
|
|
64
|
-
if filename in ("requirements.txt", "package.json", "pipfile", "pyproject.toml"):
|
|
65
|
-
if not self._has_lock_file(path):
|
|
66
|
-
findings.append(Finding(
|
|
67
|
-
engine=self.name,
|
|
68
|
-
severity="info",
|
|
69
|
-
title="Missing lock file",
|
|
70
|
-
description="No lock file found for dependency pinning",
|
|
71
|
-
location=path,
|
|
72
|
-
metadata={"confidence": 0.9, "file": filename, "cwe": "CWE-1104"},
|
|
73
|
-
))
|
|
74
|
-
|
|
75
|
-
return EngineResult(engine=self.name, findings=findings, ok=True)
|
|
76
|
-
|
|
77
|
-
def _parse_requirements(self, text: str) -> dict:
|
|
78
|
-
deps = {}
|
|
79
|
-
for line in text.splitlines():
|
|
80
|
-
line = line.strip()
|
|
81
|
-
if not line or line.startswith("#") or line.startswith("-"):
|
|
82
|
-
continue
|
|
83
|
-
match = re.match(r"^([A-Za-z0-9_\-\.]+)(?:[=<>!~]+([A-Za-z0-9_\-\.]+))?", line)
|
|
84
|
-
if match:
|
|
85
|
-
name = match.group(1).lower()
|
|
86
|
-
version = match.group(2) or "unknown"
|
|
87
|
-
deps[name] = version
|
|
88
|
-
return deps
|
|
89
|
-
|
|
90
|
-
def _parse_pyproject_toml(self, text: str) -> dict:
|
|
91
|
-
deps = {}
|
|
92
|
-
try:
|
|
93
|
-
data = tomllib.loads(text)
|
|
94
|
-
for section in ("project", "tool.poetry"):
|
|
95
|
-
if section in data:
|
|
96
|
-
dep_section = data[section].get("dependencies", {})
|
|
97
|
-
for name, spec in dep_section.items():
|
|
98
|
-
if name.lower() != "python":
|
|
99
|
-
version = spec if isinstance(spec, str) else spec.get("version", "unknown")
|
|
100
|
-
deps[name.lower()] = version
|
|
101
|
-
except Exception:
|
|
102
|
-
pass
|
|
103
|
-
return deps
|
|
104
|
-
|
|
105
|
-
def _parse_package_json(self, text: str) -> dict:
|
|
106
|
-
deps = {}
|
|
107
|
-
try:
|
|
108
|
-
data = json.loads(text)
|
|
109
|
-
for section in ("dependencies", "devDependencies"):
|
|
110
|
-
for name, version in data.get(section, {}).items():
|
|
111
|
-
deps[name.lower()] = version.lstrip("^~>=")
|
|
112
|
-
except Exception:
|
|
113
|
-
pass
|
|
114
|
-
return deps
|
|
115
|
-
|
|
116
|
-
def _parse_pipfile(self, text: str) -> dict:
|
|
117
|
-
deps = {}
|
|
118
|
-
current_section = None
|
|
119
|
-
for line in text.splitlines():
|
|
120
|
-
line = line.strip()
|
|
121
|
-
if line.startswith("[") and line.endswith("]"):
|
|
122
|
-
current_section = line[1:-1].lower()
|
|
123
|
-
continue
|
|
124
|
-
if current_section in ("packages", "dev-packages") and "=" in line:
|
|
125
|
-
parts = line.split("=", 1)
|
|
126
|
-
name = parts[0].strip().lower()
|
|
127
|
-
version = parts[1].strip().strip('"\'')
|
|
128
|
-
deps[name] = version
|
|
129
|
-
return deps
|
|
130
|
-
|
|
131
|
-
def _parse_lock_file(self, text: str, filename: str) -> dict:
|
|
132
|
-
deps = {}
|
|
133
|
-
try:
|
|
134
|
-
if filename == "poetry.lock":
|
|
135
|
-
data = tomllib.loads(text)
|
|
136
|
-
for pkg in data.get("package", []):
|
|
137
|
-
deps[pkg["name"].lower()] = pkg["version"]
|
|
138
|
-
elif filename == "pnpm-lock.yaml":
|
|
139
|
-
for line in text.splitlines():
|
|
140
|
-
match = re.match(r"^\s+([^:]+):\s+\"([^\"]+)\"", line)
|
|
141
|
-
if match and not match.group(1).startswith(" "):
|
|
142
|
-
deps[match.group(1).lower()] = match.group(2)
|
|
143
|
-
elif filename == "yarn.lock":
|
|
144
|
-
for line in text.splitlines():
|
|
145
|
-
match = re.match(r'^"([^@]+)@[^"]+":', line)
|
|
146
|
-
if match:
|
|
147
|
-
name = match.group(1).lower()
|
|
148
|
-
version_match = re.search(r'version\s+"([^"]+)"', line)
|
|
149
|
-
if version_match:
|
|
150
|
-
deps[name] = version_match.group(1)
|
|
151
|
-
except Exception:
|
|
152
|
-
pass
|
|
153
|
-
return deps
|
|
154
|
-
|
|
155
|
-
def _check_vulnerabilities(self, name: str, version: str, path: str):
|
|
156
|
-
findings = []
|
|
157
|
-
name_lower = name.lower()
|
|
158
|
-
if name_lower in self.VULN_DB and version != "unknown":
|
|
159
|
-
for fixed_version, cve, affected_version in self.VULN_DB[name_lower]:
|
|
160
|
-
if self._version_less_than(version, fixed_version):
|
|
161
|
-
findings.append(Finding(
|
|
162
|
-
engine=self.name,
|
|
163
|
-
severity="high",
|
|
164
|
-
title=f"Vulnerable dependency: {name}",
|
|
165
|
-
description=f"{name} {version} is vulnerable to {cve}",
|
|
166
|
-
location=path,
|
|
167
|
-
metadata={
|
|
168
|
-
"confidence": 0.9,
|
|
169
|
-
"package": name,
|
|
170
|
-
"version": version,
|
|
171
|
-
"vuln": cve,
|
|
172
|
-
"fixed_in": fixed_version,
|
|
173
|
-
"cwe": "CWE-1104",
|
|
174
|
-
"cvss": 7.5,
|
|
175
|
-
},
|
|
176
|
-
))
|
|
177
|
-
return findings
|
|
178
|
-
|
|
179
|
-
def _check_license_conflicts(self, name: str, version: str, path: str):
|
|
180
|
-
findings = []
|
|
181
|
-
if name_lower in {"python-gnupg", "pygpl", "gpl-package"}:
|
|
182
|
-
findings.append(Finding(
|
|
183
|
-
engine=self.name,
|
|
184
|
-
severity="medium",
|
|
185
|
-
title=f"GPL/AGPL dependency detected: {name}",
|
|
186
|
-
description=f"Dependency {name} uses GPL/AGPL license which may conflict with project license",
|
|
187
|
-
location=path,
|
|
188
|
-
metadata={
|
|
189
|
-
"confidence": 0.75,
|
|
190
|
-
"package": name,
|
|
191
|
-
"license": "GPL-3.0",
|
|
192
|
-
"cwe": "CWE-1104",
|
|
193
|
-
},
|
|
194
|
-
))
|
|
195
|
-
return findings
|
|
196
|
-
|
|
197
|
-
def _check_outdated(self, name: str, version: str, path: str):
|
|
198
|
-
findings = []
|
|
199
|
-
major_match = re.match(r"^(\d+)\.", version)
|
|
200
|
-
if major_match:
|
|
201
|
-
major = int(major_match.group(1))
|
|
202
|
-
if major >= 2:
|
|
203
|
-
findings.append(Finding(
|
|
204
|
-
engine=self.name,
|
|
205
|
-
severity="low",
|
|
206
|
-
title=f"Potentially outdated dependency: {name}",
|
|
207
|
-
description=f"{name} {version} may be outdated (major version >= 2, consider checking for updates)",
|
|
208
|
-
location=path,
|
|
209
|
-
metadata={
|
|
210
|
-
"confidence": 0.6,
|
|
211
|
-
"package": name,
|
|
212
|
-
"version": version,
|
|
213
|
-
"note": "Offline heuristic - verify manually",
|
|
214
|
-
},
|
|
215
|
-
))
|
|
216
|
-
return findings
|
|
217
|
-
|
|
218
|
-
def _has_lock_file(self, path: str) -> bool:
|
|
219
|
-
p = Path(path)
|
|
220
|
-
parent = p.parent
|
|
221
|
-
lock_files = ["package-lock.json", "yarn.lock", "pnpm-lock.yaml", "poetry.lock", "Pipfile.lock"]
|
|
222
|
-
return any((parent / lf).exists() for lf in lock_files)
|
|
223
|
-
|
|
224
|
-
def _version_less_than(self, v1: str, v2: str) -> bool:
|
|
225
|
-
try:
|
|
226
|
-
parts1 = [int(x) for x in re.split(r"[.\-]", v1) if x.isdigit()]
|
|
227
|
-
parts2 = [int(x) for x in re.split(r"[.\-]", v2) if x.isdigit()]
|
|
228
|
-
for i in range(max(len(parts1), len(parts2))):
|
|
229
|
-
p1 = parts1[i] if i < len(parts1) else 0
|
|
230
|
-
p2 = parts2[i] if i < len(parts2) else 0
|
|
231
|
-
if p1 < p2:
|
|
232
|
-
return True
|
|
233
|
-
elif p1 > p2:
|
|
234
|
-
return False
|
|
235
|
-
return False
|
|
236
|
-
except Exception:
|
|
237
|
-
return False
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
_name_lower = None # placeholder to avoid unused warning in _check_vulnerabilities
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
def _version_key(v):
|
|
244
|
-
return tuple(int(x) for x in re.split(r"[.\-]", v) if x.isdigit())
|