@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,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 []
@@ -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