@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,284 @@
|
|
|
1
|
+
"""Sandbox escape pattern detection engine."""
|
|
2
|
+
import ast
|
|
3
|
+
import re
|
|
4
|
+
from typing import Dict, List, Set, Optional, Any, Tuple
|
|
5
|
+
from dataclasses import dataclass, field
|
|
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
|
+
ESCAPE_PATTERNS = {
|
|
12
|
+
"import_os": {
|
|
13
|
+
"patterns": [
|
|
14
|
+
r"__import__\s*\(\s*['\"]os['\"]",
|
|
15
|
+
r"__import__\s*\(\s*['\"]subprocess['\"]",
|
|
16
|
+
r"__import__\s*\(\s*['\"]sys['\"]",
|
|
17
|
+
r"importlib\.import_module\s*\(\s*['\"]os['\"]",
|
|
18
|
+
r"importlib\.import_module\s*\(\s*['\"]subprocess['\"]",
|
|
19
|
+
r"importlib\.import_module\s*\(\s*['\"]sys['\"]",
|
|
20
|
+
r"runpy\.run_module\s*\(\s*['\"]os['\"]",
|
|
21
|
+
r"runpy\.run_module\s*\(\s*['\"]subprocess['\"]",
|
|
22
|
+
],
|
|
23
|
+
"cwe": "CWE-829",
|
|
24
|
+
"severity": "critical",
|
|
25
|
+
"description": "Dynamic import of dangerous module (os, subprocess, sys)",
|
|
26
|
+
},
|
|
27
|
+
"builtin_access": {
|
|
28
|
+
"patterns": [
|
|
29
|
+
r"getattr\s*\(\s*__builtins__\s*,\s*['\"]eval['\"]",
|
|
30
|
+
r"getattr\s*\(\s*__builtins__\s*,\s*['\"]exec['\"]",
|
|
31
|
+
r"getattr\s*\(\s*__builtins__\s*,\s*['\"]compile['\"]",
|
|
32
|
+
r"__builtins__\s*\[\s*['\"]eval['\"]",
|
|
33
|
+
r"__builtins__\s*\[\s*['\"]exec['\"]",
|
|
34
|
+
r"__builtins__\s*\[\s*['\"]compile['\"]",
|
|
35
|
+
r"vars\s*\(\s*\)\s*\[\s*['\"]eval['\"]",
|
|
36
|
+
r"vars\s*\(\s*\)\s*\[\s*['\"]exec['\"]",
|
|
37
|
+
r"globals\s*\(\s*\)\s*\[\s*['\"]eval['\"]",
|
|
38
|
+
r"globals\s*\(\s*\)\s*\[\s*['\"]exec['\"]",
|
|
39
|
+
],
|
|
40
|
+
"cwe": "CWE-94",
|
|
41
|
+
"severity": "critical",
|
|
42
|
+
"description": "Access to builtin eval/exec/compile via getattr or dict access",
|
|
43
|
+
},
|
|
44
|
+
"eval_compile": {
|
|
45
|
+
"patterns": [
|
|
46
|
+
r"eval\s*\(\s*compile\s*\(",
|
|
47
|
+
r"exec\s*\(\s*compile\s*\(",
|
|
48
|
+
r"compile\s*\([^)]+,\s*['\"]eval['\"]",
|
|
49
|
+
r"compile\s*\([^)]+,\s*['\"]exec['\"]",
|
|
50
|
+
r"compile\s*\([^)]+,\s*['\"]single['\"]",
|
|
51
|
+
],
|
|
52
|
+
"cwe": "CWE-94",
|
|
53
|
+
"severity": "critical",
|
|
54
|
+
"description": "Code execution via eval/exec with compile",
|
|
55
|
+
},
|
|
56
|
+
"code_type": {
|
|
57
|
+
"patterns": [
|
|
58
|
+
r"types\.CodeType\s*\(",
|
|
59
|
+
r"code\.replace\s*\(",
|
|
60
|
+
r"func_code\s*=",
|
|
61
|
+
r"__code__\s*=",
|
|
62
|
+
r"f\.f_code\s*=",
|
|
63
|
+
r"frame\.f_code\s*=",
|
|
64
|
+
],
|
|
65
|
+
"cwe": "CWE-94",
|
|
66
|
+
"severity": "high",
|
|
67
|
+
"description": "Direct code object manipulation",
|
|
68
|
+
},
|
|
69
|
+
"ctypes": {
|
|
70
|
+
"patterns": [
|
|
71
|
+
r"ctypes\.CDLL\s*\(",
|
|
72
|
+
r"ctypes\.windll\s*\.",
|
|
73
|
+
r"ctypes\.WinDLL\s*\(",
|
|
74
|
+
r"ctypes\.cdll\s*\.",
|
|
75
|
+
r"ctypes\.pythonapi",
|
|
76
|
+
r"ctypes\.PyDLL\s*\(",
|
|
77
|
+
r"ctypes\.CFUNCTYPE\s*\(",
|
|
78
|
+
r"ctypes\.cast\s*\(",
|
|
79
|
+
r"ctypes\.addressof\s*\(",
|
|
80
|
+
r"ctypes\.string_at\s*\(",
|
|
81
|
+
],
|
|
82
|
+
"cwe": "CWE-111",
|
|
83
|
+
"severity": "critical",
|
|
84
|
+
"description": "ctypes usage for direct memory/library access",
|
|
85
|
+
},
|
|
86
|
+
"introspection": {
|
|
87
|
+
"patterns": [
|
|
88
|
+
r"sys\._getframe\s*\(",
|
|
89
|
+
r"inspect\.getouterframes\s*\(",
|
|
90
|
+
r"inspect\.currentframe\s*\(",
|
|
91
|
+
r"inspect\.stack\s*\(",
|
|
92
|
+
r"inspect\.trace\s*\(",
|
|
93
|
+
r"sys\.modules\s*\[\s*['\"]os['\"]",
|
|
94
|
+
r"sys\.modules\s*\[\s*['\"]subprocess['\"]",
|
|
95
|
+
],
|
|
96
|
+
"cwe": "CWE-470",
|
|
97
|
+
"severity": "high",
|
|
98
|
+
"description": "Frame/introspection access for escaping sandboxes",
|
|
99
|
+
},
|
|
100
|
+
"class_mro": {
|
|
101
|
+
"patterns": [
|
|
102
|
+
r"__class__\.__bases__",
|
|
103
|
+
r"__class__\.__mro__",
|
|
104
|
+
r"__mro__\s*\[",
|
|
105
|
+
r"__subclasses__\s*\(",
|
|
106
|
+
r"\.__subclasses__\s*\(",
|
|
107
|
+
r"type\(\)\.__subclasses__",
|
|
108
|
+
r"object\.__subclasses__\s*\(",
|
|
109
|
+
],
|
|
110
|
+
"cwe": "CWE-470",
|
|
111
|
+
"severity": "critical",
|
|
112
|
+
"description": "Class hierarchy traversal for sandbox escape (Python 2/3)",
|
|
113
|
+
},
|
|
114
|
+
"pickle": {
|
|
115
|
+
"patterns": [
|
|
116
|
+
r"pickle\.loads\s*\(",
|
|
117
|
+
r"pickle\.load\s*\(",
|
|
118
|
+
r"_pickle\.loads\s*\(",
|
|
119
|
+
r"marshal\.loads\s*\(",
|
|
120
|
+
r"shelve\.open\s*\(",
|
|
121
|
+
r"dill\.loads\s*\(",
|
|
122
|
+
],
|
|
123
|
+
"cwe": "CWE-502",
|
|
124
|
+
"severity": "high",
|
|
125
|
+
"description": "Deserialization of untrusted data",
|
|
126
|
+
},
|
|
127
|
+
"subprocess": {
|
|
128
|
+
"patterns": [
|
|
129
|
+
r"subprocess\.call\s*\(",
|
|
130
|
+
r"subprocess\.run\s*\(",
|
|
131
|
+
r"subprocess\.Popen\s*\(",
|
|
132
|
+
r"subprocess\.check_output\s*\(",
|
|
133
|
+
r"subprocess\.check_call\s*\(",
|
|
134
|
+
r"os\.system\s*\(",
|
|
135
|
+
r"os\.popen\s*\(",
|
|
136
|
+
r"commands\.getstatusoutput\s*\(",
|
|
137
|
+
r"commands\.getoutput\s*\(",
|
|
138
|
+
],
|
|
139
|
+
"cwe": "CWE-78",
|
|
140
|
+
"severity": "critical",
|
|
141
|
+
"description": "Command execution via subprocess/os",
|
|
142
|
+
},
|
|
143
|
+
"template_injection": {
|
|
144
|
+
"patterns": [
|
|
145
|
+
r"render_template_string\s*\(",
|
|
146
|
+
r"Template\s*\([^)]*request",
|
|
147
|
+
r"Environment\s*\(\s*loader\s*=",
|
|
148
|
+
r"jinja2\.Environment\s*\(",
|
|
149
|
+
r"markupsafe\.Markup\s*\(",
|
|
150
|
+
r"Markup\s*\(",
|
|
151
|
+
],
|
|
152
|
+
"cwe": "CWE-79",
|
|
153
|
+
"severity": "high",
|
|
154
|
+
"description": "Server-side template injection (SSTI)",
|
|
155
|
+
},
|
|
156
|
+
"breakpoint": {
|
|
157
|
+
"patterns": [
|
|
158
|
+
r"breakpoint\s*\(",
|
|
159
|
+
r"pdb\.set_trace\s*\(",
|
|
160
|
+
r"ipdb\.set_trace\s*\(",
|
|
161
|
+
r"debugpy\.breakpoint\s*\(",
|
|
162
|
+
],
|
|
163
|
+
"cwe": "CWE-489",
|
|
164
|
+
"severity": "medium",
|
|
165
|
+
"description": "Debug breakpoint in production code",
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class SandboxVisitor(ast.NodeVisitor):
|
|
171
|
+
def __init__(self, filename: str, source_lines: List[str]):
|
|
172
|
+
self.filename = filename
|
|
173
|
+
self.source_lines = source_lines
|
|
174
|
+
self.findings: List[Finding] = []
|
|
175
|
+
self.seen_findings: Set[Tuple[str, int]] = set()
|
|
176
|
+
self.compiled_patterns = {
|
|
177
|
+
name: [re.compile(p, re.IGNORECASE) for p in data["patterns"]]
|
|
178
|
+
for name, data in ESCAPE_PATTERNS.items()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
def get_snippet(self, line_no: int, context: int = 2) -> str:
|
|
182
|
+
start = max(0, line_no - 1 - context)
|
|
183
|
+
end = min(len(self.source_lines), line_no + context)
|
|
184
|
+
return "\n".join(self.source_lines[start:end])
|
|
185
|
+
|
|
186
|
+
def check_node(self, node: ast.AST):
|
|
187
|
+
line_no = getattr(node, 'lineno', 0)
|
|
188
|
+
if not line_no or line_no > len(self.source_lines):
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
line_text = self.source_lines[line_no - 1]
|
|
192
|
+
for category, patterns in self.compiled_patterns.items():
|
|
193
|
+
if (category, line_no) in self.seen_findings:
|
|
194
|
+
continue
|
|
195
|
+
for pattern in patterns:
|
|
196
|
+
if pattern.search(line_text):
|
|
197
|
+
self.seen_findings.add((category, line_no))
|
|
198
|
+
data = ESCAPE_PATTERNS[category]
|
|
199
|
+
confidence = 0.9 if category in ("class_mro", "ctypes", "subprocess") else 0.85
|
|
200
|
+
|
|
201
|
+
self.findings.append(Finding(
|
|
202
|
+
engine="sandbox_escape",
|
|
203
|
+
severity=data["severity"],
|
|
204
|
+
title=f"Sandbox escape: {data['description']}",
|
|
205
|
+
description=f"Detected {category}: {data['description']}",
|
|
206
|
+
location=f"{self.filename}:{line_no}",
|
|
207
|
+
metadata={
|
|
208
|
+
"snippet": self.get_snippet(line_no),
|
|
209
|
+
"cwe": data["cwe"],
|
|
210
|
+
"confidence": confidence,
|
|
211
|
+
"category": category,
|
|
212
|
+
"pattern": pattern.pattern,
|
|
213
|
+
}
|
|
214
|
+
))
|
|
215
|
+
break
|
|
216
|
+
|
|
217
|
+
def visit_Call(self, node: ast.Call):
|
|
218
|
+
self.check_node(node)
|
|
219
|
+
self.generic_visit(node)
|
|
220
|
+
|
|
221
|
+
def visit_Attribute(self, node: ast.Attribute):
|
|
222
|
+
self.check_node(node)
|
|
223
|
+
self.generic_visit(node)
|
|
224
|
+
|
|
225
|
+
def visit_Import(self, node: ast.Import):
|
|
226
|
+
self.check_node(node)
|
|
227
|
+
self.generic_visit(node)
|
|
228
|
+
|
|
229
|
+
def visit_ImportFrom(self, node: ast.ImportFrom):
|
|
230
|
+
self.check_node(node)
|
|
231
|
+
self.generic_visit(node)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class Engine:
|
|
235
|
+
name = "sandbox_escape"
|
|
236
|
+
version = "1.0.0"
|
|
237
|
+
|
|
238
|
+
async def check_availability(self) -> bool:
|
|
239
|
+
return True
|
|
240
|
+
|
|
241
|
+
def resource_limits(self) -> ResourceLimits:
|
|
242
|
+
return ResourceLimits(max_cpu_seconds=60, max_memory_mb=512, max_files=50000)
|
|
243
|
+
|
|
244
|
+
async def run(self, ctx, config: EngineConfig) -> EngineResult:
|
|
245
|
+
# Handle both single IR and list of IRs
|
|
246
|
+
if not isinstance(ctx, list):
|
|
247
|
+
ctx = [ctx]
|
|
248
|
+
all_findings = []
|
|
249
|
+
errors = []
|
|
250
|
+
files_processed = 0
|
|
251
|
+
|
|
252
|
+
exclude_patterns = config.toggles.get("exclude", []) if config.toggles else []
|
|
253
|
+
|
|
254
|
+
for ir in ctx:
|
|
255
|
+
try:
|
|
256
|
+
if not ir.metadata.get("language") == "python":
|
|
257
|
+
continue
|
|
258
|
+
|
|
259
|
+
rel_path = ir.metadata.get("path", "unknown.py")
|
|
260
|
+
if any(pattern in rel_path for pattern in exclude_patterns):
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
content = ir.content.decode("utf-8", errors="replace")
|
|
264
|
+
source_lines = content.splitlines()
|
|
265
|
+
tree = ast.parse(content, filename=rel_path)
|
|
266
|
+
|
|
267
|
+
visitor = SandboxVisitor(rel_path, source_lines)
|
|
268
|
+
visitor.visit(tree)
|
|
269
|
+
|
|
270
|
+
all_findings.extend(visitor.findings)
|
|
271
|
+
files_processed += 1
|
|
272
|
+
|
|
273
|
+
except SyntaxError as e:
|
|
274
|
+
errors.append(f"Syntax error in {ir.metadata.get('path', 'unknown')}: {e}")
|
|
275
|
+
except Exception as e:
|
|
276
|
+
errors.append(f"Error processing {ir.metadata.get('path', 'unknown')}: {e}")
|
|
277
|
+
|
|
278
|
+
return EngineResult(
|
|
279
|
+
engine=self.name,
|
|
280
|
+
findings=all_findings,
|
|
281
|
+
errors=errors,
|
|
282
|
+
metrics={"files_processed": files_processed, "findings_count": len(all_findings)},
|
|
283
|
+
ok=len(errors) == 0
|
|
284
|
+
)
|
|
Binary file
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Full SAST engine - primary security engine with comprehensive pattern detection."""
|
|
2
|
+
import re
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from orion.engine.config import EngineConfig
|
|
9
|
+
from orion.engine.ir import OrionIR
|
|
10
|
+
from orion.engine.results import EngineResult, Finding
|
|
11
|
+
from orion.engine.sast.patterns import (
|
|
12
|
+
PATTERNS,
|
|
13
|
+
get_patterns_for_language,
|
|
14
|
+
run_ast_patterns,
|
|
15
|
+
Severity,
|
|
16
|
+
Pattern,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class SASTFinding:
|
|
22
|
+
pattern: Pattern
|
|
23
|
+
line: int
|
|
24
|
+
snippet: str
|
|
25
|
+
match: str
|
|
26
|
+
filepath: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SASTEngine:
|
|
30
|
+
name = "sast"
|
|
31
|
+
version = "2.0.0"
|
|
32
|
+
|
|
33
|
+
EXCLUDED_DIRS = {
|
|
34
|
+
"__pycache__", ".git", ".venv", "venv", "env", "node_modules",
|
|
35
|
+
"dist", "build", ".pytest_cache", ".mypy_cache", ".tox",
|
|
36
|
+
"migrations", "tests", "test", "scripts", "examples", "docs",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
EXCLUDED_FILES = {
|
|
40
|
+
"conftest.py", "manage.py", "wsgi.py", "asgi.py", "celery.py",
|
|
41
|
+
"gunicorn.conf.py", "pytest.ini", "setup.py", "setup.cfg",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async def check_availability(self) -> bool:
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
def resource_limits(self):
|
|
48
|
+
from orion.engine.config import ResourceLimits
|
|
49
|
+
return ResourceLimits(max_cpu_seconds=120, max_memory_mb=1024)
|
|
50
|
+
|
|
51
|
+
def _should_exclude(self, filepath: str, config: EngineConfig) -> bool:
|
|
52
|
+
path = Path(filepath)
|
|
53
|
+
# Check excluded directories
|
|
54
|
+
for part in path.parts:
|
|
55
|
+
if part in self.EXCLUDED_DIRS:
|
|
56
|
+
return True
|
|
57
|
+
# Check excluded files
|
|
58
|
+
if path.name in self.EXCLUDED_FILES:
|
|
59
|
+
return True
|
|
60
|
+
# Check config exclude patterns
|
|
61
|
+
if config.toggles.get("exclude"):
|
|
62
|
+
for pattern in config.toggles["exclude"]:
|
|
63
|
+
if path.match(pattern) or any(p.match(pattern) for p in path.parents):
|
|
64
|
+
return True
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
def _detect_language(self, filepath: str, content: str) -> str:
|
|
68
|
+
ext = Path(filepath).suffix.lower()
|
|
69
|
+
lang_map = {
|
|
70
|
+
".py": "python",
|
|
71
|
+
".js": "javascript",
|
|
72
|
+
".ts": "typescript",
|
|
73
|
+
".jsx": "javascript",
|
|
74
|
+
".tsx": "typescript",
|
|
75
|
+
".java": "java",
|
|
76
|
+
".go": "go",
|
|
77
|
+
".rs": "rust",
|
|
78
|
+
".php": "php",
|
|
79
|
+
".rb": "ruby",
|
|
80
|
+
".cs": "csharp",
|
|
81
|
+
".cpp": "cpp",
|
|
82
|
+
".cc": "cpp",
|
|
83
|
+
".c": "c",
|
|
84
|
+
".h": "c",
|
|
85
|
+
".hpp": "cpp",
|
|
86
|
+
".sh": "bash",
|
|
87
|
+
".yaml": "yaml",
|
|
88
|
+
".yml": "yaml",
|
|
89
|
+
".json": "json",
|
|
90
|
+
".toml": "toml",
|
|
91
|
+
".xml": "xml",
|
|
92
|
+
".html": "html",
|
|
93
|
+
".css": "css",
|
|
94
|
+
".sql": "sql",
|
|
95
|
+
}
|
|
96
|
+
return lang_map.get(ext, "unknown")
|
|
97
|
+
|
|
98
|
+
def _find_matches(self, content: str, patterns: List[Pattern], filepath: str) -> List[SASTFinding]:
|
|
99
|
+
findings = []
|
|
100
|
+
lines = content.splitlines()
|
|
101
|
+
for pattern in patterns:
|
|
102
|
+
for match in pattern.pattern.finditer(content):
|
|
103
|
+
line_num = content[:match.start()].count('\n') + 1
|
|
104
|
+
start = max(0, line_num - 3)
|
|
105
|
+
end = min(len(lines), line_num + 2)
|
|
106
|
+
snippet = '\n'.join(lines[start:end])
|
|
107
|
+
findings.append(SASTFinding(
|
|
108
|
+
pattern=pattern,
|
|
109
|
+
line=line_num,
|
|
110
|
+
snippet=snippet,
|
|
111
|
+
match=match.group()[:100],
|
|
112
|
+
filepath=filepath,
|
|
113
|
+
))
|
|
114
|
+
return findings
|
|
115
|
+
|
|
116
|
+
def _run_ast_analysis(self, content: str, filepath: str, language: str) -> List[SASTFinding]:
|
|
117
|
+
if language != "python":
|
|
118
|
+
return []
|
|
119
|
+
ast_findings = run_ast_patterns(content, filepath)
|
|
120
|
+
results = []
|
|
121
|
+
for af in ast_findings:
|
|
122
|
+
# Convert to pattern-like object
|
|
123
|
+
class ASTPattern:
|
|
124
|
+
def __init__(self, finding):
|
|
125
|
+
self.name = finding['pattern']
|
|
126
|
+
self.severity = finding['severity']
|
|
127
|
+
self.confidence = finding['confidence']
|
|
128
|
+
self.cwe = finding['cwe']
|
|
129
|
+
self.cvss = finding['cvss']
|
|
130
|
+
self.description = finding['description']
|
|
131
|
+
pattern_obj = ASTPattern(af)
|
|
132
|
+
results.append(SASTFinding(
|
|
133
|
+
pattern=pattern_obj,
|
|
134
|
+
line=af['line'],
|
|
135
|
+
snippet=af['snippet'],
|
|
136
|
+
match=af['pattern'],
|
|
137
|
+
filepath=filepath,
|
|
138
|
+
))
|
|
139
|
+
return results
|
|
140
|
+
|
|
141
|
+
def _aggregate_findings(self, findings: List[SASTFinding]) -> List[Finding]:
|
|
142
|
+
"""Aggregate findings, deduplicate similar ones, and create EngineResult findings."""
|
|
143
|
+
aggregated = []
|
|
144
|
+
seen = set()
|
|
145
|
+
|
|
146
|
+
for f in findings:
|
|
147
|
+
key = (f.filepath, f.line, f.pattern.name)
|
|
148
|
+
if key in seen:
|
|
149
|
+
continue
|
|
150
|
+
seen.add(key)
|
|
151
|
+
|
|
152
|
+
if f.pattern.confidence < 0.75:
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
severity = f.pattern.severity.value if hasattr(f.pattern.severity, 'value') else str(f.pattern.severity)
|
|
156
|
+
|
|
157
|
+
aggregated.append(Finding(
|
|
158
|
+
engine=self.name,
|
|
159
|
+
severity=severity,
|
|
160
|
+
title=f"{f.pattern.name}: {f.pattern.description}",
|
|
161
|
+
description=f"{f.pattern.description} (CWE: {f.pattern.cwe}, CVSS: {f.pattern.cvss})",
|
|
162
|
+
location=f"{f.filepath}:{f.line}",
|
|
163
|
+
metadata={
|
|
164
|
+
"pattern": f.pattern.name,
|
|
165
|
+
"cwe": f.pattern.cwe,
|
|
166
|
+
"cvss": f.pattern.cvss,
|
|
167
|
+
"confidence": f.pattern.confidence,
|
|
168
|
+
"match": f.match,
|
|
169
|
+
"snippet": f.snippet,
|
|
170
|
+
"file": f.filepath,
|
|
171
|
+
"line": f.line,
|
|
172
|
+
},
|
|
173
|
+
))
|
|
174
|
+
return aggregated
|
|
175
|
+
|
|
176
|
+
async def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
|
|
177
|
+
try:
|
|
178
|
+
content = ir.content.decode("utf-8", errors="replace")
|
|
179
|
+
except Exception:
|
|
180
|
+
content = str(ir.content)
|
|
181
|
+
|
|
182
|
+
filepath = ir.metadata.get("path", "unknown")
|
|
183
|
+
project_root = ir.metadata.get("project_root", ".")
|
|
184
|
+
language = ir.metadata.get("language") or self._detect_language(filepath, content)
|
|
185
|
+
|
|
186
|
+
if self._should_exclude(filepath, config):
|
|
187
|
+
return EngineResult(engine=self.name, ok=True, metrics={"skipped": True})
|
|
188
|
+
|
|
189
|
+
all_findings = []
|
|
190
|
+
|
|
191
|
+
# Run regex patterns
|
|
192
|
+
patterns = get_patterns_for_language(language)
|
|
193
|
+
regex_findings = self._find_matches(content, patterns, filepath)
|
|
194
|
+
all_findings.extend(regex_findings)
|
|
195
|
+
|
|
196
|
+
# Run AST patterns for Python
|
|
197
|
+
ast_findings = self._run_ast_analysis(content, filepath, language)
|
|
198
|
+
all_findings.extend(ast_findings)
|
|
199
|
+
|
|
200
|
+
# Aggregate and filter
|
|
201
|
+
findings = self._aggregate_findings(all_findings)
|
|
202
|
+
|
|
203
|
+
return EngineResult(
|
|
204
|
+
engine=self.name,
|
|
205
|
+
findings=findings,
|
|
206
|
+
ok=True,
|
|
207
|
+
metrics={
|
|
208
|
+
"files_scanned": 1,
|
|
209
|
+
"patterns_run": len(patterns),
|
|
210
|
+
"findings_count": len(findings),
|
|
211
|
+
"language": language,
|
|
212
|
+
},
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# Backward compatibility
|
|
217
|
+
Engine = SASTEngine
|