@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,316 @@
1
+ """External contract analysis engine - HTTP call security scanning."""
2
+ import ast
3
+ import re
4
+ from orion.engine.config import EngineConfig
5
+ from orion.engine.ir import OrionIR
6
+ from orion.engine.results import EngineResult, Finding
7
+
8
+
9
+ class Engine:
10
+ name = "contract_probe"
11
+ version = "1.0.0"
12
+
13
+ HTTP_CALL_PATTERNS = [
14
+ ("requests", "get"),
15
+ ("requests", "post"),
16
+ ("requests", "put"),
17
+ ("requests", "delete"),
18
+ ("requests", "patch"),
19
+ ("requests", "request"),
20
+ ("httpx", "get"),
21
+ ("httpx", "post"),
22
+ ("httpx", "put"),
23
+ ("httpx", "delete"),
24
+ ("httpx", "patch"),
25
+ ("httpx", "request"),
26
+ ("httpx.AsyncClient", "get"),
27
+ ("httpx.AsyncClient", "post"),
28
+ ("httpx.AsyncClient", "put"),
29
+ ("httpx.AsyncClient", "delete"),
30
+ ("httpx.AsyncClient", "patch"),
31
+ ("httpx.AsyncClient", "request"),
32
+ ("urllib.request", "urlopen"),
33
+ ("urllib.request", "Request"),
34
+ ("aiohttp.ClientSession", "get"),
35
+ ("aiohttp.ClientSession", "post"),
36
+ ("aiohttp.ClientSession", "put"),
37
+ ("aiohttp.ClientSession", "delete"),
38
+ ("aiohttp.ClientSession", "patch"),
39
+ ("aiohttp.ClientSession", "request"),
40
+ ]
41
+
42
+ async def check_availability(self) -> bool:
43
+ return True
44
+
45
+ def resource_limits(self):
46
+ from orion.engine.config import ResourceLimits
47
+ return ResourceLimits(max_cpu_seconds=60, max_memory_mb=256)
48
+
49
+ async def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
50
+ try:
51
+ text = ir.content.decode("utf-8", errors="replace")
52
+ except Exception:
53
+ text = str(ir.content)
54
+
55
+ path = ir.metadata.get("path", "unknown")
56
+ findings = []
57
+
58
+ code_extensions = {".py", ".js", ".jsx", ".ts", ".tsx", ".go", ".java", ".rs", ".rb", ".php", ".cs"}
59
+ if not any(path.endswith(ext) for ext in code_extensions):
60
+ return EngineResult(engine=self.name, findings=[], ok=True)
61
+
62
+ if path.endswith(".py"):
63
+ findings.extend(self._analyze_python(text, path))
64
+ else:
65
+ findings.extend(self._analyze_generic(text, path))
66
+
67
+ return EngineResult(engine=self.name, findings=findings, ok=True)
68
+
69
+ def _analyze_python(self, text: str, path: str):
70
+ findings = []
71
+ try:
72
+ tree = ast.parse(text)
73
+ except SyntaxError:
74
+ return self._analyze_generic(text, path)
75
+
76
+ lines = text.splitlines()
77
+
78
+ for node in ast.walk(tree):
79
+ if isinstance(node, ast.Call):
80
+ findings.extend(self._check_call(node, lines, path))
81
+
82
+ return findings
83
+
84
+ def _check_call(self, node: ast.Call, lines: list, path: str):
85
+ findings = []
86
+
87
+ func_name = self._get_call_name(node)
88
+ if not func_name:
89
+ return findings
90
+
91
+ # Match genuine HTTP clients and libraries
92
+ http_clients = ("requests", "httpx", "urllib.request", "aiohttp", "client", "session", "http_client", "api_client")
93
+ is_http_call = False
94
+ for mod, method in self.HTTP_CALL_PATTERNS:
95
+ if func_name == f"{mod}.{method}":
96
+ is_http_call = True
97
+ break
98
+ if not is_http_call:
99
+ parts = func_name.split(".")
100
+ if len(parts) >= 2 and parts[-1] in ("get", "post", "put", "delete", "patch", "request"):
101
+ caller = ".".join(parts[:-1]).lower()
102
+ if any(caller == c or caller.endswith(f".{c}") for c in http_clients):
103
+ is_http_call = True
104
+
105
+ if not is_http_call:
106
+ return findings
107
+
108
+ line_no = getattr(node, "lineno", 0)
109
+ snippet = self._get_snippet(lines, line_no)
110
+
111
+ missing = []
112
+ confidence_factors = []
113
+
114
+ if not self._has_timeout(node):
115
+ missing.append("timeout")
116
+ confidence_factors.append(0.8)
117
+
118
+ if not self._has_retry(node):
119
+ missing.append("retry/backoff")
120
+ confidence_factors.append(0.7)
121
+
122
+ if not self._has_max_size(node):
123
+ missing.append("max response size")
124
+ confidence_factors.append(0.7)
125
+
126
+ if not self._has_circuit_breaker(node):
127
+ missing.append("circuit breaker")
128
+ confidence_factors.append(0.6)
129
+
130
+ if self._has_verify_false(node):
131
+ findings.append(Finding(
132
+ engine=self.name,
133
+ severity="high",
134
+ title="SSL verification disabled",
135
+ description=f"HTTP call {func_name} has verify=False, disabling certificate validation",
136
+ location=f"{path}:{line_no}",
137
+ metadata={
138
+ "confidence": 0.95,
139
+ "snippet": snippet,
140
+ "call": func_name,
141
+ "issue": "verify=False",
142
+ "cwe": "CWE-295",
143
+ "cvss": 7.4,
144
+ },
145
+ ))
146
+
147
+ if self._has_hardcoded_secret_url(node):
148
+ findings.append(Finding(
149
+ engine=self.name,
150
+ severity="high",
151
+ title="Hardcoded URL with potential secrets",
152
+ description=f"HTTP call {func_name} contains hardcoded URL with embedded credentials",
153
+ location=f"{path}:{line_no}",
154
+ metadata={
155
+ "confidence": 0.85,
156
+ "snippet": snippet,
157
+ "call": func_name,
158
+ "issue": "hardcoded_secret_in_url",
159
+ "cwe": "CWE-798",
160
+ "cvss": 7.5,
161
+ },
162
+ ))
163
+
164
+ if not self._has_auth_header(node) and self._is_external_call(node):
165
+ missing.append("authentication header")
166
+ confidence_factors.append(0.75)
167
+
168
+ if missing:
169
+ confidence = max(confidence_factors) if confidence_factors else 0.75
170
+ findings.append(Finding(
171
+ engine=self.name,
172
+ severity="medium",
173
+ title=f"Missing HTTP protections in {func_name}",
174
+ description=f"External call missing: {', '.join(missing)}",
175
+ location=f"{path}:{line_no}",
176
+ metadata={
177
+ "confidence": round(confidence, 2),
178
+ "snippet": snippet,
179
+ "call": func_name,
180
+ "missing": missing,
181
+ "cwe": "CWE-1188",
182
+ "cvss": 5.3,
183
+ },
184
+ ))
185
+
186
+ return findings
187
+
188
+ def _get_call_name(self, node) -> str:
189
+ if isinstance(node, ast.Call):
190
+ return self._get_call_name(node.func)
191
+ if isinstance(node, ast.Attribute):
192
+ if isinstance(node.value, ast.Attribute):
193
+ return f"{self._get_call_name(node.value)}.{node.attr}"
194
+ elif isinstance(node.value, ast.Name):
195
+ return f"{node.value.id}.{node.attr}"
196
+ elif isinstance(node.value, ast.Call):
197
+ return f"{self._get_call_name(node.value)}.{node.attr}"
198
+ elif isinstance(node, ast.Name):
199
+ return node.id
200
+ return ""
201
+
202
+ def _has_timeout(self, node: ast.Call) -> bool:
203
+ for kw in node.keywords:
204
+ if kw.arg in ("timeout", "timeout_seconds", "read_timeout", "connect_timeout"):
205
+ return True
206
+ return False
207
+
208
+ def _has_retry(self, node: ast.Call) -> bool:
209
+ for kw in node.keywords:
210
+ if kw.arg in ("retries", "max_retries", "retry", "backoff_factor", "backoff"):
211
+ return True
212
+ return False
213
+
214
+ def _has_max_size(self, node: ast.Call) -> bool:
215
+ for kw in node.keywords:
216
+ if kw.arg in ("max_size", "max_content_size", "stream"):
217
+ return True
218
+ return False
219
+
220
+ def _has_circuit_breaker(self, node: ast.Call) -> bool:
221
+ for kw in node.keywords:
222
+ if kw.arg in ("circuit_breaker", "breaker", "failure_threshold"):
223
+ return True
224
+ return False
225
+
226
+ def _has_verify_false(self, node: ast.Call) -> bool:
227
+ for kw in node.keywords:
228
+ if kw.arg == "verify":
229
+ if isinstance(kw.value, ast.Constant) and kw.value.value is False:
230
+ return True
231
+ return False
232
+
233
+ def _has_hardcoded_secret_url(self, node: ast.Call) -> bool:
234
+ for arg in node.args:
235
+ if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
236
+ url = arg.value
237
+ if re.search(r"https?://[^:]+:[^@]+@", url):
238
+ return True
239
+ if re.search(r"(api[_-]?key|token|secret|password)=[^&]+", url):
240
+ return True
241
+ for kw in node.keywords:
242
+ if kw.arg in ("url", "uri") and isinstance(kw.value, ast.Constant):
243
+ url = kw.value.value
244
+ if re.search(r"https?://[^:]+:[^@]+@", url):
245
+ return True
246
+ return False
247
+
248
+ def _has_auth_header(self, node: ast.Call) -> bool:
249
+ for kw in node.keywords:
250
+ if kw.arg in ("headers", "auth"):
251
+ if isinstance(kw.value, ast.Dict):
252
+ for key in kw.value.keys:
253
+ if isinstance(key, ast.Constant) and isinstance(key.value, str):
254
+ if key.value.lower() in ("authorization", "x-api-key", "api-key", "bearer"):
255
+ return True
256
+ elif isinstance(kw.value, ast.Call):
257
+ if isinstance(kw.value.func, ast.Name) and kw.value.func.id in ("HTTPBasicAuth", "HTTPDigestAuth", "BearerToken"):
258
+ return True
259
+ return False
260
+
261
+ def _is_external_call(self, node: ast.Call) -> bool:
262
+ for arg in node.args:
263
+ if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
264
+ url = arg.value
265
+ if url.startswith("http://") or url.startswith("https://"):
266
+ if not any(x in url for x in ["localhost", "127.0.0.1", "0.0.0.0", "[::1]"]):
267
+ return True
268
+ for kw in node.keywords:
269
+ if kw.arg in ("url", "uri") and isinstance(kw.value, ast.Constant):
270
+ url = kw.value.value
271
+ if url.startswith("http://") or url.startswith("https://"):
272
+ if not any(x in url for x in ["localhost", "127.0.0.1", "0.0.0.0", "[::1]"]):
273
+ return True
274
+ return False
275
+
276
+ def _analyze_generic(self, text: str, path: str):
277
+ findings = []
278
+ lines = text.splitlines()
279
+
280
+ http_patterns = [
281
+ r"requests\.(?:get|post|put|delete|patch|request)\s*\(",
282
+ r"httpx\.(?:get|post|put|delete|patch|request)\s*\(",
283
+ r"fetch\s*\(",
284
+ r"axios\.(?:get|post|put|delete|patch|request)\s*\(",
285
+ r"curl\s+",
286
+ r"urllib\.request\.urlopen\s*\(",
287
+ ]
288
+
289
+ for i, line in enumerate(lines):
290
+ for pattern in http_patterns:
291
+ if re.search(pattern, line):
292
+ if "timeout" not in line and "retry" not in line:
293
+ findings.append(Finding(
294
+ engine=self.name,
295
+ severity="medium",
296
+ title="Potential HTTP call missing protections",
297
+ description=f"HTTP call pattern detected without visible timeout/retry",
298
+ location=f"{path}:{i+1}",
299
+ metadata={
300
+ "confidence": 0.6,
301
+ "snippet": line.strip()[:200],
302
+ "pattern": pattern,
303
+ "cwe": "CWE-1188",
304
+ "cvss": 5.3,
305
+ },
306
+ ))
307
+ break
308
+
309
+ return findings
310
+
311
+ def _get_snippet(self, lines: list, line_no: int, context: int = 2) -> str:
312
+ if line_no <= 0:
313
+ return ""
314
+ start = max(0, line_no - context - 1)
315
+ end = min(len(lines), line_no + context)
316
+ return "\n".join(lines[start:end])
@@ -0,0 +1,75 @@
1
+ import asyncio
2
+ import logging
3
+ import time
4
+ from typing import Dict, List, Callable, Optional
5
+
6
+ from orion.engine.config import EngineConfig
7
+ from orion.engine.ir import OrionIR
8
+ from orion.engine.results import EngineResult, Finding
9
+ from orion.engine.registry import EngineRegistry
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class OrionEngine:
15
+ def __init__(self, registry: EngineRegistry | None = None):
16
+ self._registry = registry or EngineRegistry()
17
+ self._engines: Dict[str, type] = {}
18
+ self._engines.update(self._registry.all())
19
+
20
+ def register_engine(self, name: str, engine_cls: type) -> None:
21
+ self._engines[name] = engine_cls
22
+
23
+ @property
24
+ def registered_count(self) -> int:
25
+ return len(self._engines)
26
+
27
+ async def run(
28
+ self,
29
+ irs: List[OrionIR],
30
+ config: EngineConfig,
31
+ on_progress: Optional[Callable[[int, int, str, str], None]] = None
32
+ ) -> EngineResult:
33
+ start = time.monotonic()
34
+ all_findings: List[Finding] = []
35
+ all_errors: List[str] = []
36
+ engines_run = 0
37
+
38
+ # Initialize engines once
39
+ instantiated_engines = {}
40
+ for name, cls in self._engines.items():
41
+ try:
42
+ instantiated_engines[name] = cls()
43
+ except Exception as e:
44
+ all_errors.append(f"Init {name}: {e}")
45
+
46
+ total_files = len(irs)
47
+ # Process IRs with live progress reporting
48
+ for idx, ir in enumerate(irs, 1):
49
+ path = ir.metadata.get("path", "")
50
+ if on_progress:
51
+ on_progress(idx, total_files, path, "Scanning 42 Engines")
52
+
53
+ for name, engine in instantiated_engines.items():
54
+ try:
55
+ res = await engine.run(ir, config)
56
+ engines_run += 1
57
+ if res and res.findings:
58
+ all_findings.extend(res.findings)
59
+ if res and res.errors:
60
+ all_errors.extend([f"{name}: {e}" for e in res.errors])
61
+ except Exception as exc:
62
+ all_errors.append(f"{name}: {exc}")
63
+
64
+ if on_progress:
65
+ on_progress(total_files, total_files, "Finalizing", "Aggregating Findings & Metrics")
66
+
67
+ elapsed = time.monotonic() - start
68
+
69
+ return EngineResult(
70
+ engine="orion",
71
+ findings=all_findings,
72
+ errors=all_errors,
73
+ metrics={"elapsed_s": round(elapsed, 3), "engines_run": engines_run, "files_scanned": len(irs)},
74
+ ok=not all_errors,
75
+ )
@@ -0,0 +1,134 @@
1
+ """Cryptographic Flaws, Weak Ciphers, and Key Hygiene Auditor.
2
+
3
+ 100% Custom In-House Developed Engine for Orion Engine Platform.
4
+ Detects broken hashes (MD5, SHA1), weak ciphers (DES, RC4, ECB), insecure PRNGs, and short key lengths.
5
+ """
6
+ import re
7
+ from pathlib import Path
8
+ from typing import List, Dict, Any
9
+
10
+ from orion.engine.config import EngineConfig, ResourceLimits
11
+ from orion.engine.ir import OrionIR
12
+ from orion.engine.results import EngineResult, Finding
13
+
14
+
15
+ CRYPTO_RULES = [
16
+ {
17
+ "id": "CRYPTO-001",
18
+ "pattern": r"(?i)\b(hashlib\.(md5|sha1)|MD5\.new|SHA1\.new|MessageDigest\.getInstance\s*\(\s*[\"'](MD5|SHA-1)[\"']|createHash\s*\(\s*[\"'](md5|sha1)[\"'])\b",
19
+ "title": "Use of cryptographically broken hash algorithm (MD5/SHA-1)",
20
+ "description": "MD5 and SHA-1 have known collision vulnerabilities. Use SHA-256, SHA-3, or BLAKE2 instead",
21
+ "severity": "high",
22
+ "cwe": "CWE-328",
23
+ "confidence": 0.95,
24
+ },
25
+ {
26
+ "id": "CRYPTO-002",
27
+ "pattern": r"(?i)\b(AES\.MODE_ECB|Cipher\.getInstance\s*\(\s*[\"']AES/ECB/|mode\s*=\s*AES\.MODE_ECB)\b",
28
+ "title": "Insecure ECB cipher mode used",
29
+ "description": "Electronic Codebook (ECB) mode does not provide pattern confidentiality. Use GCM or CBC with random IV",
30
+ "severity": "critical",
31
+ "cwe": "CWE-327",
32
+ "confidence": 0.95,
33
+ },
34
+ {
35
+ "id": "CRYPTO-003",
36
+ "pattern": r"(?i)\b(DES|Blowfish|RC4|ARC4|TripleDES)\.(new|getInstance|createCipher)\b",
37
+ "title": "Use of deprecated weak symmetric cipher (DES/RC4/Blowfish)",
38
+ "description": "Legacy ciphers with short block sizes or known biases are susceptible to plaintext recovery attacks",
39
+ "severity": "critical",
40
+ "cwe": "CWE-327",
41
+ "confidence": 0.95,
42
+ },
43
+ {
44
+ "id": "CRYPTO-004",
45
+ "pattern": r"(?i)\b(random\.(randint|random|choice|choices|sample|getrandbits)|Math\.random\s*\(\s*\)|rand\s*\(\s*\)|srand\s*\()\b.*(?i)(token|key|secret|password|auth|salt|nonce|session)",
46
+ "title": "Insecure Pseudo-Random Number Generator used for security context",
47
+ "description": "Standard pseudo-random generators are predictable. Use secrets module, crypto.getRandomValues, or CSPRNG",
48
+ "severity": "high",
49
+ "cwe": "CWE-338",
50
+ "confidence": 0.85,
51
+ },
52
+ {
53
+ "id": "CRYPTO-005",
54
+ "pattern": r"(?i)(iv|nonce|salt)\s*=\s*(b?[\"'][a-zA-Z0-9_\-]{8,}[\"']|b?\\x[0-9a-fA-F]{2})",
55
+ "title": "Static or hardcoded IV / Salt detected",
56
+ "description": "Initialization vectors (IVs) and salts must be uniquely and randomly generated for each encryption operation",
57
+ "severity": "high",
58
+ "cwe": "CWE-329",
59
+ "confidence": 0.85,
60
+ },
61
+ {
62
+ "id": "CRYPTO-006",
63
+ "pattern": r"(?i)(RSA\.generate|generate_private_key|KeyPairGenerator\.getInstance)\s*\(\s*(512|1024)\b",
64
+ "title": "Insufficient RSA key length (< 2048 bits)",
65
+ "description": "RSA keys smaller than 2048 bits are vulnerable to factorization. Minimum recommended key size is 2048 or 4096 bits",
66
+ "severity": "high",
67
+ "cwe": "CWE-326",
68
+ "confidence": 0.95,
69
+ },
70
+ ]
71
+
72
+
73
+ class CryptoGuardEngine:
74
+ """100% custom, in-house developed Cryptographic Flaw & Key Auditor."""
75
+
76
+ name = "crypto_guard"
77
+ version = "1.0.0"
78
+
79
+ def __init__(self):
80
+ self._compiled_rules = []
81
+ for r in CRYPTO_RULES:
82
+ self._compiled_rules.append({
83
+ **r,
84
+ "regex": re.compile(r["pattern"])
85
+ })
86
+
87
+ async def check_availability(self) -> bool:
88
+ return True
89
+
90
+ def resource_limits(self) -> ResourceLimits:
91
+ return ResourceLimits(max_cpu_seconds=30, max_memory_mb=256)
92
+
93
+ async def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
94
+ path = ir.metadata.get("path", "unknown")
95
+ try:
96
+ content = ir.content.decode("utf-8", errors="replace")
97
+ except Exception:
98
+ return EngineResult(engine=self.name, findings=[], ok=True)
99
+
100
+ lines = content.splitlines()
101
+ findings = []
102
+
103
+ for rule in self._compiled_rules:
104
+ for match in rule["regex"].finditer(content):
105
+ line_no = content[:match.start()].count('\n') + 1
106
+ start_line = max(0, line_no - 2)
107
+ end_line = min(len(lines), line_no + 2)
108
+ snippet = '\n'.join(lines[start_line:end_line])
109
+
110
+ findings.append(Finding(
111
+ engine=self.name,
112
+ severity=rule["severity"],
113
+ title=f"{rule['id']}: {rule['title']}",
114
+ description=rule["description"],
115
+ location=f"{path}:{line_no}",
116
+ metadata={
117
+ "rule_id": rule["id"],
118
+ "cwe": rule["cwe"],
119
+ "confidence": rule["confidence"],
120
+ "snippet": snippet,
121
+ "file": path,
122
+ "line": line_no,
123
+ }
124
+ ))
125
+
126
+ return EngineResult(
127
+ engine=self.name,
128
+ findings=findings,
129
+ ok=True,
130
+ metrics={"findings_count": len(findings)}
131
+ )
132
+
133
+
134
+ Engine = CryptoGuardEngine