@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,513 @@
|
|
|
1
|
+
"""SAST pattern detectors for security vulnerabilities."""
|
|
2
|
+
import re
|
|
3
|
+
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import List, Optional, Pattern
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Severity(Enum):
|
|
10
|
+
CRITICAL = "critical"
|
|
11
|
+
HIGH = "high"
|
|
12
|
+
MEDIUM = "medium"
|
|
13
|
+
LOW = "low"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Pattern:
|
|
18
|
+
name: str
|
|
19
|
+
pattern: Pattern
|
|
20
|
+
severity: Severity
|
|
21
|
+
confidence: float
|
|
22
|
+
cwe: str
|
|
23
|
+
cvss: float
|
|
24
|
+
description: str
|
|
25
|
+
languages: List[str]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _compile_patterns() -> List[Pattern]:
|
|
29
|
+
return [
|
|
30
|
+
# SQL Injection
|
|
31
|
+
Pattern(
|
|
32
|
+
name="sql_injection_concat",
|
|
33
|
+
pattern=re.compile(
|
|
34
|
+
r'(execute|executemany|query|raw)\s*\(\s*["\'].*[%+].*["\']',
|
|
35
|
+
re.IGNORECASE,
|
|
36
|
+
),
|
|
37
|
+
severity=Severity.CRITICAL,
|
|
38
|
+
confidence=0.85,
|
|
39
|
+
cwe="CWE-89",
|
|
40
|
+
cvss=9.8,
|
|
41
|
+
description="SQL query built via string concatenation - potential SQL injection",
|
|
42
|
+
languages=["python"],
|
|
43
|
+
),
|
|
44
|
+
Pattern(
|
|
45
|
+
name="sql_injection_format",
|
|
46
|
+
pattern=re.compile(
|
|
47
|
+
r'(execute|executemany|query|raw)\s*\(\s*f["\'].*\{.*\}.*["\']',
|
|
48
|
+
re.IGNORECASE,
|
|
49
|
+
),
|
|
50
|
+
severity=Severity.CRITICAL,
|
|
51
|
+
confidence=0.9,
|
|
52
|
+
cwe="CWE-89",
|
|
53
|
+
cvss=9.8,
|
|
54
|
+
description="SQL query built via f-string - potential SQL injection",
|
|
55
|
+
languages=["python"],
|
|
56
|
+
),
|
|
57
|
+
Pattern(
|
|
58
|
+
name="sql_injection_percent",
|
|
59
|
+
pattern=re.compile(
|
|
60
|
+
r'(execute|executemany|query|raw)\s*\(\s*["\'].*%.*["\']\s*%',
|
|
61
|
+
re.IGNORECASE,
|
|
62
|
+
),
|
|
63
|
+
severity=Severity.CRITICAL,
|
|
64
|
+
confidence=0.8,
|
|
65
|
+
cwe="CWE-89",
|
|
66
|
+
cvss=9.8,
|
|
67
|
+
description="SQL query built via % formatting - potential SQL injection",
|
|
68
|
+
languages=["python"],
|
|
69
|
+
),
|
|
70
|
+
# XSS
|
|
71
|
+
Pattern(
|
|
72
|
+
name="xss_direct_output",
|
|
73
|
+
pattern=re.compile(
|
|
74
|
+
r'(render_template_string|mark_safe|safe_filter|escape\(false\))',
|
|
75
|
+
re.IGNORECASE,
|
|
76
|
+
),
|
|
77
|
+
severity=Severity.HIGH,
|
|
78
|
+
confidence=0.8,
|
|
79
|
+
cwe="CWE-79",
|
|
80
|
+
cvss=7.1,
|
|
81
|
+
description="Direct output of user-controlled data without escaping",
|
|
82
|
+
languages=["python", "javascript", "typescript"],
|
|
83
|
+
),
|
|
84
|
+
Pattern(
|
|
85
|
+
name="xss_inner_html",
|
|
86
|
+
pattern=re.compile(
|
|
87
|
+
r'innerHTML\s*=\s*.*request\.(GET|POST|body|params|query)',
|
|
88
|
+
re.IGNORECASE,
|
|
89
|
+
),
|
|
90
|
+
severity=Severity.HIGH,
|
|
91
|
+
confidence=0.85,
|
|
92
|
+
cwe="CWE-79",
|
|
93
|
+
cvss=7.1,
|
|
94
|
+
description="User input directly assigned to innerHTML",
|
|
95
|
+
languages=["javascript", "typescript"],
|
|
96
|
+
),
|
|
97
|
+
# Command Injection
|
|
98
|
+
Pattern(
|
|
99
|
+
name="cmd_injection_os_system",
|
|
100
|
+
pattern=re.compile(
|
|
101
|
+
r'os\.system\s*\(.*[\+\%]',
|
|
102
|
+
re.IGNORECASE,
|
|
103
|
+
),
|
|
104
|
+
severity=Severity.CRITICAL,
|
|
105
|
+
confidence=0.9,
|
|
106
|
+
cwe="CWE-78",
|
|
107
|
+
cvss=9.8,
|
|
108
|
+
description="Command injection via os.system with string concatenation",
|
|
109
|
+
languages=["python"],
|
|
110
|
+
),
|
|
111
|
+
Pattern(
|
|
112
|
+
name="cmd_injection_subprocess",
|
|
113
|
+
pattern=re.compile(
|
|
114
|
+
r'subprocess\.(call|run|Popen|check_output)\s*\(.*shell\s*=\s*True.*[\+\%]',
|
|
115
|
+
re.IGNORECASE,
|
|
116
|
+
),
|
|
117
|
+
severity=Severity.CRITICAL,
|
|
118
|
+
confidence=0.85,
|
|
119
|
+
cwe="CWE-78",
|
|
120
|
+
cvss=9.8,
|
|
121
|
+
description="Command injection via subprocess with shell=True and concatenation",
|
|
122
|
+
languages=["python"],
|
|
123
|
+
),
|
|
124
|
+
Pattern(
|
|
125
|
+
name="cmd_injection_popen",
|
|
126
|
+
pattern=re.compile(
|
|
127
|
+
r'popen\s*\(.*[\+\%]',
|
|
128
|
+
re.IGNORECASE,
|
|
129
|
+
),
|
|
130
|
+
severity=Severity.CRITICAL,
|
|
131
|
+
confidence=0.8,
|
|
132
|
+
cwe="CWE-78",
|
|
133
|
+
cvss=9.8,
|
|
134
|
+
description="Command injection via popen with concatenation",
|
|
135
|
+
languages=["python"],
|
|
136
|
+
),
|
|
137
|
+
# SSRF
|
|
138
|
+
Pattern(
|
|
139
|
+
name="ssrf_requests",
|
|
140
|
+
pattern=re.compile(
|
|
141
|
+
r'(requests|httpx|aiohttp|urllib)\.(get|post|put|delete|request)\s*\(.*(input|request\.(GET|POST|body|params|query)|argv)',
|
|
142
|
+
re.IGNORECASE,
|
|
143
|
+
),
|
|
144
|
+
severity=Severity.HIGH,
|
|
145
|
+
confidence=0.75,
|
|
146
|
+
cwe="CWE-918",
|
|
147
|
+
cvss=7.5,
|
|
148
|
+
description="Server-side request forgery - user-controlled URL",
|
|
149
|
+
languages=["python"],
|
|
150
|
+
),
|
|
151
|
+
Pattern(
|
|
152
|
+
name="ssrf_urlopen",
|
|
153
|
+
pattern=re.compile(
|
|
154
|
+
r'urllib\.request\.urlopen\s*\(.*(input|request\.(GET|POST|body|params|query)|argv)',
|
|
155
|
+
re.IGNORECASE,
|
|
156
|
+
),
|
|
157
|
+
severity=Severity.HIGH,
|
|
158
|
+
confidence=0.75,
|
|
159
|
+
cwe="CWE-918",
|
|
160
|
+
cvss=7.5,
|
|
161
|
+
description="SSRF via urllib.urlopen with user input",
|
|
162
|
+
languages=["python"],
|
|
163
|
+
),
|
|
164
|
+
# Path Traversal
|
|
165
|
+
Pattern(
|
|
166
|
+
name="path_traversal_open",
|
|
167
|
+
pattern=re.compile(
|
|
168
|
+
r'open\s*\(.*[\+\%].*\.\./',
|
|
169
|
+
re.IGNORECASE,
|
|
170
|
+
),
|
|
171
|
+
severity=Severity.HIGH,
|
|
172
|
+
confidence=0.8,
|
|
173
|
+
cwe="CWE-22",
|
|
174
|
+
cvss=7.5,
|
|
175
|
+
description="Path traversal via open() with user-controlled path",
|
|
176
|
+
languages=["python"],
|
|
177
|
+
),
|
|
178
|
+
Pattern(
|
|
179
|
+
name="path_traversal_pathlib",
|
|
180
|
+
pattern=re.compile(
|
|
181
|
+
r'Path\s*\(.*[\+\%].*\.\./',
|
|
182
|
+
re.IGNORECASE,
|
|
183
|
+
),
|
|
184
|
+
severity=Severity.HIGH,
|
|
185
|
+
confidence=0.75,
|
|
186
|
+
cwe="CWE-22",
|
|
187
|
+
cvss=7.5,
|
|
188
|
+
description="Path traversal via pathlib.Path with user-controlled path",
|
|
189
|
+
languages=["python"],
|
|
190
|
+
),
|
|
191
|
+
Pattern(
|
|
192
|
+
name="path_traversal_join",
|
|
193
|
+
pattern=re.compile(
|
|
194
|
+
r'os\.path\.join\s*\(.*\.\..*\)',
|
|
195
|
+
re.IGNORECASE,
|
|
196
|
+
),
|
|
197
|
+
severity=Severity.MEDIUM,
|
|
198
|
+
confidence=0.7,
|
|
199
|
+
cwe="CWE-22",
|
|
200
|
+
cvss=5.9,
|
|
201
|
+
description="Potential path traversal via os.path.join with ../",
|
|
202
|
+
languages=["python"],
|
|
203
|
+
),
|
|
204
|
+
# Hardcoded Credentials
|
|
205
|
+
Pattern(
|
|
206
|
+
name="hardcoded_password",
|
|
207
|
+
pattern=re.compile(
|
|
208
|
+
r'(password|passwd|pwd)\s*=\s*["\'][^"\']{8,}["\']',
|
|
209
|
+
re.IGNORECASE,
|
|
210
|
+
),
|
|
211
|
+
severity=Severity.HIGH,
|
|
212
|
+
confidence=0.75,
|
|
213
|
+
cwe="CWE-798",
|
|
214
|
+
cvss=7.5,
|
|
215
|
+
description="Hardcoded password in source code",
|
|
216
|
+
languages=["python", "javascript", "typescript", "yaml", "json"],
|
|
217
|
+
),
|
|
218
|
+
Pattern(
|
|
219
|
+
name="hardcoded_api_key",
|
|
220
|
+
pattern=re.compile(
|
|
221
|
+
r'(api[_-]?key|apikey|secret[_-]?key|access[_-]?token)\s*=\s*["\'][^"\']{16,}["\']',
|
|
222
|
+
re.IGNORECASE,
|
|
223
|
+
),
|
|
224
|
+
severity=Severity.CRITICAL,
|
|
225
|
+
confidence=0.85,
|
|
226
|
+
cwe="CWE-798",
|
|
227
|
+
cvss=9.1,
|
|
228
|
+
description="Hardcoded API key or secret token",
|
|
229
|
+
languages=["python", "javascript", "typescript", "yaml", "json"],
|
|
230
|
+
),
|
|
231
|
+
Pattern(
|
|
232
|
+
name="hardcoded_aws_key",
|
|
233
|
+
pattern=re.compile(
|
|
234
|
+
r'(AKIA[0-9A-Z]{16}|aws[_-]?access[_-]?key[_-]?id)\s*=\s*["\'][^"\']+["\']',
|
|
235
|
+
re.IGNORECASE,
|
|
236
|
+
),
|
|
237
|
+
severity=Severity.CRITICAL,
|
|
238
|
+
confidence=0.95,
|
|
239
|
+
cwe="CWE-798",
|
|
240
|
+
cvss=9.8,
|
|
241
|
+
description="Hardcoded AWS access key",
|
|
242
|
+
languages=["python", "javascript", "typescript", "yaml", "json"],
|
|
243
|
+
),
|
|
244
|
+
Pattern(
|
|
245
|
+
name="hardcoded_private_key",
|
|
246
|
+
pattern=re.compile(
|
|
247
|
+
r'-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----',
|
|
248
|
+
re.IGNORECASE,
|
|
249
|
+
),
|
|
250
|
+
severity=Severity.CRITICAL,
|
|
251
|
+
confidence=0.99,
|
|
252
|
+
cwe="CWE-798",
|
|
253
|
+
cvss=9.8,
|
|
254
|
+
description="Hardcoded private key",
|
|
255
|
+
languages=["python", "javascript", "typescript", "yaml", "json"],
|
|
256
|
+
),
|
|
257
|
+
# Weak Crypto
|
|
258
|
+
Pattern(
|
|
259
|
+
name="weak_crypto_md5",
|
|
260
|
+
pattern=re.compile(
|
|
261
|
+
r'hashlib\.md5\s*\(',
|
|
262
|
+
re.IGNORECASE,
|
|
263
|
+
),
|
|
264
|
+
severity=Severity.MEDIUM,
|
|
265
|
+
confidence=0.9,
|
|
266
|
+
cwe="CWE-327",
|
|
267
|
+
cvss=5.3,
|
|
268
|
+
description="Use of cryptographically weak MD5 hash",
|
|
269
|
+
languages=["python"],
|
|
270
|
+
),
|
|
271
|
+
Pattern(
|
|
272
|
+
name="weak_crypto_sha1",
|
|
273
|
+
pattern=re.compile(
|
|
274
|
+
r'hashlib\.sha1\s*\(',
|
|
275
|
+
re.IGNORECASE,
|
|
276
|
+
),
|
|
277
|
+
severity=Severity.MEDIUM,
|
|
278
|
+
confidence=0.9,
|
|
279
|
+
cwe="CWE-327",
|
|
280
|
+
cvss=5.3,
|
|
281
|
+
description="Use of cryptographically weak SHA1 hash",
|
|
282
|
+
languages=["python"],
|
|
283
|
+
),
|
|
284
|
+
Pattern(
|
|
285
|
+
name="weak_crypto_des",
|
|
286
|
+
pattern=re.compile(
|
|
287
|
+
r'(DES|3DES|TripleDES)\.new\s*\(',
|
|
288
|
+
re.IGNORECASE,
|
|
289
|
+
),
|
|
290
|
+
severity=Severity.HIGH,
|
|
291
|
+
confidence=0.85,
|
|
292
|
+
cwe="CWE-327",
|
|
293
|
+
cvss=7.5,
|
|
294
|
+
description="Use of weak DES/3DES encryption",
|
|
295
|
+
languages=["python"],
|
|
296
|
+
),
|
|
297
|
+
Pattern(
|
|
298
|
+
name="weak_crypto_ecb",
|
|
299
|
+
pattern=re.compile(
|
|
300
|
+
r'\.MODE_ECB\s*\)',
|
|
301
|
+
re.IGNORECASE,
|
|
302
|
+
),
|
|
303
|
+
severity=Severity.HIGH,
|
|
304
|
+
confidence=0.85,
|
|
305
|
+
cwe="CWE-327",
|
|
306
|
+
cvss=7.5,
|
|
307
|
+
description="Use of ECB mode (insecure)",
|
|
308
|
+
languages=["python"],
|
|
309
|
+
),
|
|
310
|
+
Pattern(
|
|
311
|
+
name="weak_crypto_random",
|
|
312
|
+
pattern=re.compile(
|
|
313
|
+
r'random\.(random|randint|choice|choices)\s*\(',
|
|
314
|
+
re.IGNORECASE,
|
|
315
|
+
),
|
|
316
|
+
severity=Severity.MEDIUM,
|
|
317
|
+
confidence=0.7,
|
|
318
|
+
cwe="CWE-338",
|
|
319
|
+
cvss=5.3,
|
|
320
|
+
description="Use of non-cryptographic random for security purposes",
|
|
321
|
+
languages=["python"],
|
|
322
|
+
),
|
|
323
|
+
# Insecure Deserialization
|
|
324
|
+
Pattern(
|
|
325
|
+
name="deserialization_pickle",
|
|
326
|
+
pattern=re.compile(
|
|
327
|
+
r'pickle\.(loads|load)\s*\(',
|
|
328
|
+
re.IGNORECASE,
|
|
329
|
+
),
|
|
330
|
+
severity=Severity.CRITICAL,
|
|
331
|
+
confidence=0.9,
|
|
332
|
+
cwe="CWE-502",
|
|
333
|
+
cvss=9.8,
|
|
334
|
+
description="Insecure deserialization via pickle",
|
|
335
|
+
languages=["python"],
|
|
336
|
+
),
|
|
337
|
+
Pattern(
|
|
338
|
+
name="deserialization_yaml",
|
|
339
|
+
pattern=re.compile(
|
|
340
|
+
r'yaml\.(load|load_all)\s*\(.*Loader\s*=\s*(yaml\.)?CLoader',
|
|
341
|
+
re.IGNORECASE,
|
|
342
|
+
),
|
|
343
|
+
severity=Severity.CRITICAL,
|
|
344
|
+
confidence=0.85,
|
|
345
|
+
cwe="CWE-502",
|
|
346
|
+
cvss=9.8,
|
|
347
|
+
description="Insecure YAML deserialization with unsafe loader",
|
|
348
|
+
languages=["python"],
|
|
349
|
+
),
|
|
350
|
+
Pattern(
|
|
351
|
+
name="deserialization_json",
|
|
352
|
+
pattern=re.compile(
|
|
353
|
+
r'jsonpickle\.decode\s*\(',
|
|
354
|
+
re.IGNORECASE,
|
|
355
|
+
),
|
|
356
|
+
severity=Severity.HIGH,
|
|
357
|
+
confidence=0.8,
|
|
358
|
+
cwe="CWE-502",
|
|
359
|
+
cvss=7.5,
|
|
360
|
+
description="Insecure deserialization via jsonpickle",
|
|
361
|
+
languages=["python"],
|
|
362
|
+
),
|
|
363
|
+
Pattern(
|
|
364
|
+
name="deserialization_marshal",
|
|
365
|
+
pattern=re.compile(
|
|
366
|
+
r'marshal\.loads\s*\(',
|
|
367
|
+
re.IGNORECASE,
|
|
368
|
+
),
|
|
369
|
+
severity=Severity.HIGH,
|
|
370
|
+
confidence=0.8,
|
|
371
|
+
cwe="CWE-502",
|
|
372
|
+
cvss=7.5,
|
|
373
|
+
description="Insecure deserialization via marshal",
|
|
374
|
+
languages=["python"],
|
|
375
|
+
),
|
|
376
|
+
# Additional dangerous patterns
|
|
377
|
+
Pattern(
|
|
378
|
+
name="eval_usage",
|
|
379
|
+
pattern=re.compile(
|
|
380
|
+
r'\beval\s*\(',
|
|
381
|
+
re.IGNORECASE,
|
|
382
|
+
),
|
|
383
|
+
severity=Severity.CRITICAL,
|
|
384
|
+
confidence=0.9,
|
|
385
|
+
cwe="CWE-94",
|
|
386
|
+
cvss=9.8,
|
|
387
|
+
description="Use of eval() - code injection risk",
|
|
388
|
+
languages=["python"],
|
|
389
|
+
),
|
|
390
|
+
Pattern(
|
|
391
|
+
name="exec_usage",
|
|
392
|
+
pattern=re.compile(
|
|
393
|
+
r'\bexec\s*\(',
|
|
394
|
+
re.IGNORECASE,
|
|
395
|
+
),
|
|
396
|
+
severity=Severity.CRITICAL,
|
|
397
|
+
confidence=0.9,
|
|
398
|
+
cwe="CWE-94",
|
|
399
|
+
cvss=9.8,
|
|
400
|
+
description="Use of exec() - code injection risk",
|
|
401
|
+
languages=["python"],
|
|
402
|
+
),
|
|
403
|
+
Pattern(
|
|
404
|
+
name="subprocess_shell_true",
|
|
405
|
+
pattern=re.compile(
|
|
406
|
+
r'subprocess\.(call|run|Popen|check_output)\s*\([^)]*shell\s*=\s*True',
|
|
407
|
+
re.IGNORECASE,
|
|
408
|
+
),
|
|
409
|
+
severity=Severity.HIGH,
|
|
410
|
+
confidence=0.8,
|
|
411
|
+
cwe="CWE-78",
|
|
412
|
+
cvss=7.5,
|
|
413
|
+
description="subprocess with shell=True - command injection risk",
|
|
414
|
+
languages=["python"],
|
|
415
|
+
),
|
|
416
|
+
]
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
PATTERNS = _compile_patterns()
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def get_patterns_for_language(language: str) -> List[Pattern]:
|
|
423
|
+
"""Get patterns applicable to a specific language."""
|
|
424
|
+
return [p for p in PATTERNS if language.lower() in [l.lower() for l in p.languages]]
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def get_all_patterns() -> List[Pattern]:
|
|
428
|
+
"""Get all patterns."""
|
|
429
|
+
return PATTERNS
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class ASTPatternVisitor(ast.NodeVisitor):
|
|
433
|
+
"""AST-based pattern detection for more complex patterns."""
|
|
434
|
+
|
|
435
|
+
def __init__(self, content: str, filepath: str):
|
|
436
|
+
self.content = content
|
|
437
|
+
self.filepath = filepath
|
|
438
|
+
self.findings = []
|
|
439
|
+
self.lines = content.splitlines()
|
|
440
|
+
|
|
441
|
+
def _get_line(self, node: ast.AST) -> int:
|
|
442
|
+
return getattr(node, 'lineno', 0)
|
|
443
|
+
|
|
444
|
+
def _get_snippet(self, line_num: int, context: int = 2) -> str:
|
|
445
|
+
start = max(0, line_num - context - 1)
|
|
446
|
+
end = min(len(self.lines), line_num + context)
|
|
447
|
+
return '\n'.join(self.lines[start:end])
|
|
448
|
+
|
|
449
|
+
def visit_Call(self, node: ast.Call):
|
|
450
|
+
# Detect hardcoded secrets in function calls
|
|
451
|
+
if isinstance(node.func, ast.Attribute):
|
|
452
|
+
attr_name = node.func.attr.lower()
|
|
453
|
+
if attr_name in ('password', 'secret', 'token', 'api_key', 'apikey'):
|
|
454
|
+
for keyword in node.keywords:
|
|
455
|
+
if isinstance(keyword.value, ast.Constant) and isinstance(keyword.value.value, str):
|
|
456
|
+
if len(keyword.value.value) >= 8:
|
|
457
|
+
self.findings.append({
|
|
458
|
+
'pattern': 'hardcoded_secret_call',
|
|
459
|
+
'line': self._get_line(node),
|
|
460
|
+
'snippet': self._get_snippet(self._get_line(node)),
|
|
461
|
+
'severity': Severity.HIGH,
|
|
462
|
+
'confidence': 0.75,
|
|
463
|
+
'cwe': 'CWE-798',
|
|
464
|
+
'cvss': 7.5,
|
|
465
|
+
'description': f'Hardcoded secret in {attr_name} parameter',
|
|
466
|
+
})
|
|
467
|
+
# Detect subprocess with user input
|
|
468
|
+
if isinstance(node.func, ast.Attribute):
|
|
469
|
+
if node.func.attr in ('call', 'run', 'Popen', 'check_output'):
|
|
470
|
+
for keyword in node.keywords:
|
|
471
|
+
if keyword.arg == 'shell' and isinstance(keyword.value, ast.Constant) and keyword.value.value is True:
|
|
472
|
+
self.findings.append({
|
|
473
|
+
'pattern': 'subprocess_shell_true_ast',
|
|
474
|
+
'line': self._get_line(node),
|
|
475
|
+
'snippet': self._get_snippet(self._get_line(node)),
|
|
476
|
+
'severity': Severity.HIGH,
|
|
477
|
+
'confidence': 0.8,
|
|
478
|
+
'cwe': 'CWE-78',
|
|
479
|
+
'cvss': 7.5,
|
|
480
|
+
'description': 'subprocess with shell=True detected via AST',
|
|
481
|
+
})
|
|
482
|
+
self.generic_visit(node)
|
|
483
|
+
|
|
484
|
+
def visit_Assign(self, node: ast.Assign):
|
|
485
|
+
# Detect hardcoded credentials in assignments
|
|
486
|
+
for target in node.targets:
|
|
487
|
+
if isinstance(target, ast.Name):
|
|
488
|
+
var_name = target.id.lower()
|
|
489
|
+
if any(kw in var_name for kw in ('password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey', 'access_key')):
|
|
490
|
+
if isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
|
|
491
|
+
if len(node.value.value) >= 8:
|
|
492
|
+
self.findings.append({
|
|
493
|
+
'pattern': 'hardcoded_credential_ast',
|
|
494
|
+
'line': self._get_line(node),
|
|
495
|
+
'snippet': self._get_snippet(self._get_line(node)),
|
|
496
|
+
'severity': Severity.HIGH if 'api' in var_name or 'key' in var_name else Severity.MEDIUM,
|
|
497
|
+
'confidence': 0.8,
|
|
498
|
+
'cwe': 'CWE-798',
|
|
499
|
+
'cvss': 7.5 if 'api' in var_name or 'key' in var_name else 5.9,
|
|
500
|
+
'description': f'Hardcoded credential in variable {target.id}',
|
|
501
|
+
})
|
|
502
|
+
self.generic_visit(node)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def run_ast_patterns(content: str, filepath: str) -> List[dict]:
|
|
506
|
+
"""Run AST-based pattern detection."""
|
|
507
|
+
try:
|
|
508
|
+
tree = ast.parse(content)
|
|
509
|
+
visitor = ASTPatternVisitor(content, filepath)
|
|
510
|
+
visitor.visit(tree)
|
|
511
|
+
return visitor.findings
|
|
512
|
+
except SyntaxError:
|
|
513
|
+
return []
|
|
Binary file
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Secret detection engine - regex + entropy scanning."""
|
|
2
|
+
import re
|
|
3
|
+
from orion.engine.config import EngineConfig
|
|
4
|
+
from orion.engine.ir import OrionIR
|
|
5
|
+
from orion.engine.results import EngineResult, Finding
|
|
6
|
+
from orion.engine.secrets.patterns import scan_patterns, shannon_entropy, find_high_entropy_strings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Engine:
|
|
10
|
+
name = "secrets"
|
|
11
|
+
version = "1.0.0"
|
|
12
|
+
|
|
13
|
+
async def check_availability(self) -> bool:
|
|
14
|
+
return True
|
|
15
|
+
|
|
16
|
+
def resource_limits(self):
|
|
17
|
+
from orion.engine.config import ResourceLimits
|
|
18
|
+
return ResourceLimits(max_cpu_seconds=60, max_memory_mb=256)
|
|
19
|
+
|
|
20
|
+
async def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
|
|
21
|
+
try:
|
|
22
|
+
text = ir.content.decode("utf-8", errors="replace")
|
|
23
|
+
except Exception:
|
|
24
|
+
text = str(ir.content)
|
|
25
|
+
|
|
26
|
+
findings = []
|
|
27
|
+
path = ir.metadata.get("path", "unknown")
|
|
28
|
+
lines = text.splitlines()
|
|
29
|
+
|
|
30
|
+
for p in scan_patterns(text):
|
|
31
|
+
line_no = text[:p["start"]].count("\n") + 1
|
|
32
|
+
snippet = self._mask_secret(p["match"])
|
|
33
|
+
entropy = shannon_entropy(p["match"])
|
|
34
|
+
findings.append(Finding(
|
|
35
|
+
engine=self.name,
|
|
36
|
+
severity="high" if entropy > 4.0 else "medium",
|
|
37
|
+
title=f"Secret detected: {p['pattern_id']}",
|
|
38
|
+
description=f"Potential secret matching pattern '{p['pattern_id']}' found with entropy {entropy:.2f}",
|
|
39
|
+
location=f"{path}:{line_no}",
|
|
40
|
+
metadata={
|
|
41
|
+
"confidence": 0.85 if entropy > 4.0 else 0.7,
|
|
42
|
+
"snippet": snippet,
|
|
43
|
+
"pattern": p["pattern_id"],
|
|
44
|
+
"entropy": round(entropy, 2),
|
|
45
|
+
"cwe": "CWE-798",
|
|
46
|
+
"cvss": 7.5,
|
|
47
|
+
},
|
|
48
|
+
))
|
|
49
|
+
|
|
50
|
+
for hp in find_high_entropy_strings(text):
|
|
51
|
+
line_no = text.find(hp["string"])
|
|
52
|
+
if line_no >= 0:
|
|
53
|
+
line_no = text[:line_no].count("\n") + 1
|
|
54
|
+
else:
|
|
55
|
+
line_no = 0
|
|
56
|
+
findings.append(Finding(
|
|
57
|
+
engine=self.name,
|
|
58
|
+
severity="medium",
|
|
59
|
+
title="High entropy string detected",
|
|
60
|
+
description=f"String with Shannon entropy {hp['entropy']:.2f} may be a secret",
|
|
61
|
+
location=f"{path}:{line_no}",
|
|
62
|
+
metadata={
|
|
63
|
+
"confidence": 0.7,
|
|
64
|
+
"snippet": self._mask_secret(hp["string"]),
|
|
65
|
+
"entropy": round(hp["entropy"], 2),
|
|
66
|
+
"pattern": "high_entropy",
|
|
67
|
+
"cwe": "CWE-798",
|
|
68
|
+
"cvss": 5.3,
|
|
69
|
+
},
|
|
70
|
+
))
|
|
71
|
+
|
|
72
|
+
return EngineResult(engine=self.name, findings=findings, ok=True)
|
|
73
|
+
|
|
74
|
+
def _mask_secret(self, secret: str) -> str:
|
|
75
|
+
if len(secret) <= 8:
|
|
76
|
+
return "*" * len(secret)
|
|
77
|
+
return secret[:4] + "*" * (len(secret) - 8) + secret[-4:]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Regex + entropy-based secret detection patterns."""
|
|
2
|
+
import re
|
|
3
|
+
import math
|
|
4
|
+
from collections import Counter
|
|
5
|
+
|
|
6
|
+
_SECRET_PATTERNS = [
|
|
7
|
+
("aws_access_key", re.compile(r"\bAKIA[0-9A-Z]{16}\b")),
|
|
8
|
+
("github_token", re.compile(r"\bgh[pousr]_[A-Za-z0-9_]{36}\b")),
|
|
9
|
+
("stripe_live_key", re.compile(r"\bsk_live_[a-zA-Z0-9]{24}\b")),
|
|
10
|
+
("jwt_token", re.compile(r"\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b")),
|
|
11
|
+
("slack_token", re.compile(r"\bxox[baprs]-[0-9]{12}-[0-9]{12}-[0-9]{12}-[a-z0-9]{32}\b")),
|
|
12
|
+
("generic_api_key", re.compile(r"[a-zA-Z_]*[_-]?key[_-]?[=:]\s*['\"][a-zA-Z0-9_\-]{20,}['\"]")),
|
|
13
|
+
("private_key", re.compile(r"-----BEGIN (?:RSA|DSA|EC|OPENSSH) PRIVATE KEY-----")),
|
|
14
|
+
("generic_secret", re.compile(r"(?i)(?:api[_-]?key|apikey|secret|token|password|passwd)\s*[:=]\s*['\"]([a-zA-Z0-9_\-+/=]{20,})['\"]")),
|
|
15
|
+
("aws_secret_key", re.compile(r"(?i)aws.{0,20}?(?:secret.?access.?key)\s*[:=]\s*['\"]([A-Za-z0-9/+=]{40})['\"]")),
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
_TEST_PATTERNS = [
|
|
19
|
+
"sk_test_",
|
|
20
|
+
"AKIAIOSFODNN7EXAMPLE",
|
|
21
|
+
"example",
|
|
22
|
+
"test",
|
|
23
|
+
"dummy",
|
|
24
|
+
"placeholder",
|
|
25
|
+
"your_key_here",
|
|
26
|
+
"your_api_key",
|
|
27
|
+
"your_secret",
|
|
28
|
+
"xxxxxxxx",
|
|
29
|
+
"aaaaaaaa",
|
|
30
|
+
"12345678",
|
|
31
|
+
"fake",
|
|
32
|
+
"mock",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_test_pattern(match: str) -> bool:
|
|
37
|
+
match_lower = match.lower()
|
|
38
|
+
return any(tp.lower() in match_lower for tp in _TEST_PATTERNS)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def scan_patterns(text: str):
|
|
42
|
+
findings = []
|
|
43
|
+
for pattern_id, pattern in _SECRET_PATTERNS:
|
|
44
|
+
for match in pattern.finditer(text):
|
|
45
|
+
matched_text = match.group(0)
|
|
46
|
+
if is_test_pattern(matched_text):
|
|
47
|
+
continue
|
|
48
|
+
findings.append({
|
|
49
|
+
"pattern_id": pattern_id,
|
|
50
|
+
"match": matched_text,
|
|
51
|
+
"start": match.start(),
|
|
52
|
+
"end": match.end(),
|
|
53
|
+
})
|
|
54
|
+
return findings
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def shannon_entropy(data: str) -> float:
|
|
58
|
+
if not data or len(data) < 20:
|
|
59
|
+
return 0.0
|
|
60
|
+
counts = Counter(data)
|
|
61
|
+
length = len(data)
|
|
62
|
+
return -sum((c / length) * math.log2(c / length) for c in counts.values())
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def find_high_entropy_strings(text: str, min_length: int = 20, threshold: float = 4.5):
|
|
66
|
+
results = []
|
|
67
|
+
words = re.findall(r"[A-Za-z0-9_\-+/=]{20,}", text)
|
|
68
|
+
for word in words:
|
|
69
|
+
if is_test_pattern(word):
|
|
70
|
+
continue
|
|
71
|
+
entropy = shannon_entropy(word)
|
|
72
|
+
if entropy > threshold:
|
|
73
|
+
results.append({"string": word, "entropy": entropy})
|
|
74
|
+
return results
|