@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,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
+ )
@@ -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