@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,324 @@
|
|
|
1
|
+
"""Cross-engine correlation hunter - correlates findings across engines to find attack chains."""
|
|
2
|
+
from typing import List, Dict, Set, Any
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
|
|
6
|
+
from orion.engine.config import EngineConfig
|
|
7
|
+
from orion.engine.ir import OrionIR
|
|
8
|
+
from orion.engine.results import EngineResult, Finding
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class CorrelationRule:
|
|
13
|
+
name: str
|
|
14
|
+
required_engines: List[str]
|
|
15
|
+
required_patterns: List[str]
|
|
16
|
+
boosted_severity: str
|
|
17
|
+
description: str
|
|
18
|
+
chain_id: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HunterEngine:
|
|
22
|
+
name = "hunter"
|
|
23
|
+
version = "1.0.0"
|
|
24
|
+
|
|
25
|
+
# Correlation rules defining attack chains
|
|
26
|
+
CORRELATION_RULES = [
|
|
27
|
+
CorrelationRule(
|
|
28
|
+
name="sqli_missing_auth",
|
|
29
|
+
required_engines=["sast"],
|
|
30
|
+
required_patterns=["sql_injection", "missing_auth", "authorization_bypass"],
|
|
31
|
+
boosted_severity="critical",
|
|
32
|
+
description="SQL injection combined with missing authentication = critical data breach risk",
|
|
33
|
+
chain_id="CHAIN-001",
|
|
34
|
+
),
|
|
35
|
+
CorrelationRule(
|
|
36
|
+
name="xss_missing_csp",
|
|
37
|
+
required_engines=["sast", "config_engine"],
|
|
38
|
+
required_patterns=["xss", "missing_csp", "csp_not_enabled"],
|
|
39
|
+
boosted_severity="high",
|
|
40
|
+
description="XSS vulnerability with no Content Security Policy = high exploitability",
|
|
41
|
+
chain_id="CHAIN-002",
|
|
42
|
+
),
|
|
43
|
+
CorrelationRule(
|
|
44
|
+
name="secret_public_repo",
|
|
45
|
+
required_engines=["secrets", "deps"],
|
|
46
|
+
required_patterns=["hardcoded_secret", "public_repository", "git_public"],
|
|
47
|
+
boosted_severity="critical",
|
|
48
|
+
description="Hardcoded secret in public repository = immediate credential compromise",
|
|
49
|
+
chain_id="CHAIN-003",
|
|
50
|
+
),
|
|
51
|
+
CorrelationRule(
|
|
52
|
+
name="pii_logging",
|
|
53
|
+
required_engines=["sast", "logging_c"],
|
|
54
|
+
required_patterns=["hardcoded_pii", "pii_logged", "sensitive_data_logging"],
|
|
55
|
+
boosted_severity="high",
|
|
56
|
+
description="PII in code combined with logging = data exposure risk",
|
|
57
|
+
chain_id="CHAIN-004",
|
|
58
|
+
),
|
|
59
|
+
CorrelationRule(
|
|
60
|
+
name="monkey_patch_no_tests",
|
|
61
|
+
required_engines=["symbol", "test_coverage"],
|
|
62
|
+
required_patterns=["monkey_patch", "runtime_patch", "untested_code", "low_coverage"],
|
|
63
|
+
boosted_severity="medium",
|
|
64
|
+
description="Monkey patching without tests = high regression risk",
|
|
65
|
+
chain_id="CHAIN-005",
|
|
66
|
+
),
|
|
67
|
+
CorrelationRule(
|
|
68
|
+
name="n1_query_no_pagination",
|
|
69
|
+
required_engines=["dataflow", "sast"],
|
|
70
|
+
required_patterns=["n_plus_one_query", "missing_pagination", "unbounded_query"],
|
|
71
|
+
boosted_severity="high",
|
|
72
|
+
description="N+1 query pattern without pagination = DoS risk",
|
|
73
|
+
chain_id="CHAIN-006",
|
|
74
|
+
),
|
|
75
|
+
CorrelationRule(
|
|
76
|
+
name="ssrf_no_egress_control",
|
|
77
|
+
required_engines=["sast", "container", "config_engine"],
|
|
78
|
+
required_patterns=["ssrf", "missing_egress_control", "allow_all_outbound"],
|
|
79
|
+
boosted_severity="critical",
|
|
80
|
+
description="SSRF vulnerability with no egress filtering = internal network access",
|
|
81
|
+
chain_id="CHAIN-007",
|
|
82
|
+
),
|
|
83
|
+
CorrelationRule(
|
|
84
|
+
name="weak_crypto_sensitive_data",
|
|
85
|
+
required_engines=["sast", "data_residency"],
|
|
86
|
+
required_patterns=["weak_crypto", "md5", "sha1", "sensitive_data_storage"],
|
|
87
|
+
boosted_severity="high",
|
|
88
|
+
description="Weak cryptography protecting sensitive data = compliance violation",
|
|
89
|
+
chain_id="CHAIN-008",
|
|
90
|
+
),
|
|
91
|
+
CorrelationRule(
|
|
92
|
+
name="deserialization_rce_chain",
|
|
93
|
+
required_engines=["sast", "sandbox_escape"],
|
|
94
|
+
required_patterns=["insecure_deserialization", "pickle", "yaml_unsafe", "sandbox_escape"],
|
|
95
|
+
boosted_severity="critical",
|
|
96
|
+
description="Insecure deserialization with sandbox escape = RCE chain",
|
|
97
|
+
chain_id="CHAIN-009",
|
|
98
|
+
),
|
|
99
|
+
CorrelationRule(
|
|
100
|
+
name="path_traversal_file_read",
|
|
101
|
+
required_engines=["sast", "authorization"],
|
|
102
|
+
required_patterns=["path_traversal", "missing_path_validation", "authorization_bypass"],
|
|
103
|
+
boosted_severity="high",
|
|
104
|
+
description="Path traversal with missing authorization = arbitrary file read",
|
|
105
|
+
chain_id="CHAIN-010",
|
|
106
|
+
),
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
SEVERITY_ORDER = {"low": 0, "medium": 1, "high": 2, "critical": 3}
|
|
110
|
+
|
|
111
|
+
async def check_availability(self) -> bool:
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
def resource_limits(self):
|
|
115
|
+
from orion.engine.config import ResourceLimits
|
|
116
|
+
return ResourceLimits(max_cpu_seconds=30, max_memory_mb=128)
|
|
117
|
+
|
|
118
|
+
def _get_all_findings(self, ir: OrionIR) -> List[Finding]:
|
|
119
|
+
"""Get all findings from context - check metrics and project context."""
|
|
120
|
+
# First check if findings are in metrics
|
|
121
|
+
all_findings = ir.metadata.get("all_findings", [])
|
|
122
|
+
|
|
123
|
+
# Also check if there's a project context with findings from all engines
|
|
124
|
+
project_ctx = ir.metadata.get("project_context")
|
|
125
|
+
if project_ctx and hasattr(project_ctx, 'findings'):
|
|
126
|
+
all_findings.extend(project_ctx.findings)
|
|
127
|
+
|
|
128
|
+
return all_findings
|
|
129
|
+
|
|
130
|
+
def _extract_pattern_key(self, finding: Finding) -> str:
|
|
131
|
+
"""Extract a normalized pattern key from a finding."""
|
|
132
|
+
# Try metadata first
|
|
133
|
+
pattern = finding.metadata.get("pattern", "")
|
|
134
|
+
if pattern:
|
|
135
|
+
return pattern.lower()
|
|
136
|
+
|
|
137
|
+
# Fallback to title
|
|
138
|
+
title = finding.title.lower()
|
|
139
|
+
# Extract key terms
|
|
140
|
+
keywords = [
|
|
141
|
+
"sql_injection", "sqli", "xss", "cmd_injection", "ssrf",
|
|
142
|
+
"path_traversal", "hardcoded", "secret", "credential",
|
|
143
|
+
"weak_crypto", "md5", "sha1", "deserialization", "pickle",
|
|
144
|
+
"yaml", "eval", "exec", "subprocess", "shell",
|
|
145
|
+
"missing_auth", "authorization", "csp", "logging",
|
|
146
|
+
"monkey_patch", "n_plus_one", "n1", "pagination",
|
|
147
|
+
"egress", "sandbox", "pii", "sensitive",
|
|
148
|
+
]
|
|
149
|
+
for kw in keywords:
|
|
150
|
+
if kw in title:
|
|
151
|
+
return kw
|
|
152
|
+
return title[:50]
|
|
153
|
+
|
|
154
|
+
def _match_rule(self, rule: CorrelationRule, findings_by_engine: Dict[str, List[Finding]]) -> List[Finding]:
|
|
155
|
+
"""Check if a correlation rule matches and return boosted findings."""
|
|
156
|
+
matched_findings = []
|
|
157
|
+
|
|
158
|
+
# Check if all required engines have findings
|
|
159
|
+
for engine in rule.required_engines:
|
|
160
|
+
if engine not in findings_by_engine or not findings_by_engine[engine]:
|
|
161
|
+
return []
|
|
162
|
+
|
|
163
|
+
# Check if required patterns are present
|
|
164
|
+
all_patterns = set()
|
|
165
|
+
for engine_findings in findings_by_engine.values():
|
|
166
|
+
for f in engine_findings:
|
|
167
|
+
all_patterns.add(self._extract_pattern_key(f))
|
|
168
|
+
|
|
169
|
+
# Flexible matching - check if any required pattern is present
|
|
170
|
+
pattern_matched = False
|
|
171
|
+
for required in rule.required_patterns:
|
|
172
|
+
for pattern in all_patterns:
|
|
173
|
+
if required.lower() in pattern or pattern in required.lower():
|
|
174
|
+
pattern_matched = True
|
|
175
|
+
break
|
|
176
|
+
if pattern_matched:
|
|
177
|
+
break
|
|
178
|
+
|
|
179
|
+
if not pattern_matched:
|
|
180
|
+
return []
|
|
181
|
+
|
|
182
|
+
# Rule matched - boost severity of related findings
|
|
183
|
+
for engine in rule.required_engines:
|
|
184
|
+
for finding in findings_by_engine[engine]:
|
|
185
|
+
pattern_key = self._extract_pattern_key(finding)
|
|
186
|
+
# Check if this finding is part of the chain
|
|
187
|
+
for required in rule.required_patterns:
|
|
188
|
+
if required.lower() in pattern_key or pattern_key in required.lower():
|
|
189
|
+
original_severity = finding.severity
|
|
190
|
+
if self.SEVERITY_ORDER.get(rule.boosted_severity, 0) > self.SEVERITY_ORDER.get(original_severity, 0):
|
|
191
|
+
boosted = Finding(
|
|
192
|
+
engine=finding.engine,
|
|
193
|
+
severity=rule.boosted_severity,
|
|
194
|
+
title=f"[CORRELATED] {finding.title}",
|
|
195
|
+
description=f"{finding.description}\n\nCorrelation: {rule.description} (Chain: {rule.chain_id})",
|
|
196
|
+
location=finding.location,
|
|
197
|
+
metadata={
|
|
198
|
+
**finding.metadata,
|
|
199
|
+
"chain": rule.chain_id,
|
|
200
|
+
"chain_name": rule.name,
|
|
201
|
+
"boosted_from": original_severity,
|
|
202
|
+
"boosted_to": rule.boosted_severity,
|
|
203
|
+
"correlation_rule": rule.name,
|
|
204
|
+
"original_finding": True,
|
|
205
|
+
},
|
|
206
|
+
)
|
|
207
|
+
matched_findings.append(boosted)
|
|
208
|
+
|
|
209
|
+
return matched_findings
|
|
210
|
+
|
|
211
|
+
def _find_cross_file_chains(self, all_findings: List[Finding]) -> List[Finding]:
|
|
212
|
+
"""Find correlation chains across different files."""
|
|
213
|
+
# Group findings by file
|
|
214
|
+
findings_by_file = defaultdict(list)
|
|
215
|
+
for f in all_findings:
|
|
216
|
+
file_path = f.metadata.get("file", f.location.split(":")[0] if ":" in f.location else "unknown")
|
|
217
|
+
findings_by_file[file_path].append(f)
|
|
218
|
+
|
|
219
|
+
chain_findings = []
|
|
220
|
+
|
|
221
|
+
# Look for chains within same file
|
|
222
|
+
for file_path, findings in findings_by_file.items():
|
|
223
|
+
if len(findings) < 2:
|
|
224
|
+
continue
|
|
225
|
+
|
|
226
|
+
# Check for specific combinations in same file
|
|
227
|
+
has_sqli = any("sql_injection" in self._extract_pattern_key(f).lower() for f in findings)
|
|
228
|
+
has_auth_issue = any("auth" in self._extract_pattern_key(f).lower() for f in findings)
|
|
229
|
+
has_xss = any("xss" in self._extract_pattern_key(f).lower() for f in findings)
|
|
230
|
+
has_secret = any("secret" in self._extract_pattern_key(f).lower() or "credential" in self._extract_pattern_key(f).lower() for f in findings)
|
|
231
|
+
has_logging = any("log" in self._extract_pattern_key(f).lower() for f in findings)
|
|
232
|
+
has_pii = any("pii" in self._extract_pattern_key(f).lower() for f in findings)
|
|
233
|
+
|
|
234
|
+
if has_sqli and has_auth_issue:
|
|
235
|
+
chain_findings.append(Finding(
|
|
236
|
+
engine=self.name,
|
|
237
|
+
severity="critical",
|
|
238
|
+
title="SQLi + Auth Bypass in same file",
|
|
239
|
+
description="SQL injection vulnerability combined with authentication bypass in same file",
|
|
240
|
+
location=file_path,
|
|
241
|
+
metadata={
|
|
242
|
+
"chain": "CHAIN-011",
|
|
243
|
+
"chain_name": "sqli_auth_same_file",
|
|
244
|
+
"components": ["sql_injection", "auth_bypass"],
|
|
245
|
+
"file": file_path,
|
|
246
|
+
},
|
|
247
|
+
))
|
|
248
|
+
|
|
249
|
+
if has_xss and has_secret:
|
|
250
|
+
chain_findings.append(Finding(
|
|
251
|
+
engine=self.name,
|
|
252
|
+
severity="high",
|
|
253
|
+
title="XSS + Secret Exposure in same file",
|
|
254
|
+
description="XSS vulnerability could leak hardcoded secrets",
|
|
255
|
+
location=file_path,
|
|
256
|
+
metadata={
|
|
257
|
+
"chain": "CHAIN-012",
|
|
258
|
+
"chain_name": "xss_secret_same_file",
|
|
259
|
+
"components": ["xss", "secret_exposure"],
|
|
260
|
+
"file": file_path,
|
|
261
|
+
},
|
|
262
|
+
))
|
|
263
|
+
|
|
264
|
+
if has_pii and has_logging:
|
|
265
|
+
chain_findings.append(Finding(
|
|
266
|
+
engine=self.name,
|
|
267
|
+
severity="high",
|
|
268
|
+
title="PII + Logging in same file",
|
|
269
|
+
description="PII data being logged - potential data exposure",
|
|
270
|
+
location=file_path,
|
|
271
|
+
metadata={
|
|
272
|
+
"chain": "CHAIN-013",
|
|
273
|
+
"chain_name": "pii_logging_same_file",
|
|
274
|
+
"components": ["pii", "logging"],
|
|
275
|
+
"file": file_path,
|
|
276
|
+
},
|
|
277
|
+
))
|
|
278
|
+
|
|
279
|
+
return chain_findings
|
|
280
|
+
|
|
281
|
+
async def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
|
|
282
|
+
all_findings = self._get_all_findings(ir)
|
|
283
|
+
|
|
284
|
+
if not all_findings:
|
|
285
|
+
return EngineResult(engine=self.name, ok=True, metrics={"correlations_found": 0})
|
|
286
|
+
|
|
287
|
+
# Group findings by engine
|
|
288
|
+
findings_by_engine = defaultdict(list)
|
|
289
|
+
for f in all_findings:
|
|
290
|
+
findings_by_engine[f.engine].append(f)
|
|
291
|
+
|
|
292
|
+
# Apply correlation rules
|
|
293
|
+
correlated_findings = []
|
|
294
|
+
for rule in self.CORRELATION_RULES:
|
|
295
|
+
matched = self._match_rule(rule, findings_by_engine)
|
|
296
|
+
correlated_findings.extend(matched)
|
|
297
|
+
|
|
298
|
+
# Find cross-file chains
|
|
299
|
+
cross_file_chains = self._find_cross_file_chains(all_findings)
|
|
300
|
+
correlated_findings.extend(cross_file_chains)
|
|
301
|
+
|
|
302
|
+
# Deduplicate by chain_id and location
|
|
303
|
+
seen = set()
|
|
304
|
+
deduped = []
|
|
305
|
+
for f in correlated_findings:
|
|
306
|
+
key = (f.metadata.get("chain", ""), f.location)
|
|
307
|
+
if key not in seen:
|
|
308
|
+
seen.add(key)
|
|
309
|
+
deduped.append(f)
|
|
310
|
+
|
|
311
|
+
return EngineResult(
|
|
312
|
+
engine=self.name,
|
|
313
|
+
findings=deduped,
|
|
314
|
+
ok=True,
|
|
315
|
+
metrics={
|
|
316
|
+
"total_findings_analyzed": len(all_findings),
|
|
317
|
+
"engines_analyzed": len(findings_by_engine),
|
|
318
|
+
"correlations_found": len(deduped),
|
|
319
|
+
"rules_evaluated": len(self.CORRELATION_RULES),
|
|
320
|
+
},
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
Engine = HunterEngine
|
|
Binary file
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Infrastructure as Code (IaC) & Cloud Security Engine.
|
|
2
|
+
|
|
3
|
+
100% Custom In-House Developed Engine for Orion Engine Platform.
|
|
4
|
+
Scans Terraform, Kubernetes, Dockerfile, CloudFormation, and GitHub Actions CI/CD workflows.
|
|
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
|
+
IAC_RULES = [
|
|
16
|
+
# Dockerfile Rules
|
|
17
|
+
{
|
|
18
|
+
"id": "IAC-DOCKER-001",
|
|
19
|
+
"file_types": [".dockerfile", "dockerfile"],
|
|
20
|
+
"pattern": r"(?i)^\s*USER\s+root\b|(?i)^\s*FROM\s+.*(?!\n\s*USER)",
|
|
21
|
+
"title": "Container running as root user",
|
|
22
|
+
"description": "Container should specify a non-root USER to prevent privilege escalation attacks",
|
|
23
|
+
"severity": "high",
|
|
24
|
+
"cwe": "CWE-250",
|
|
25
|
+
"confidence": 0.85,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "IAC-DOCKER-002",
|
|
29
|
+
"file_types": [".dockerfile", "dockerfile"],
|
|
30
|
+
"pattern": r"(?i)^\s*ADD\s+https?://",
|
|
31
|
+
"title": "Insecure ADD instruction with remote URL",
|
|
32
|
+
"description": "Use curl/wget with checksum verification or COPY instead of remote ADD",
|
|
33
|
+
"severity": "medium",
|
|
34
|
+
"cwe": "CWE-829",
|
|
35
|
+
"confidence": 0.9,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "IAC-DOCKER-003",
|
|
39
|
+
"file_types": [".dockerfile", "dockerfile"],
|
|
40
|
+
"pattern": r"(?i)^\s*FROM\s+[^:\n]+(?::latest)?\s*$",
|
|
41
|
+
"title": "Unpinned base image tag",
|
|
42
|
+
"description": "Base image uses 'latest' or unpinned tag; pin image to specific SHA256 digest or version",
|
|
43
|
+
"severity": "low",
|
|
44
|
+
"cwe": "CWE-1104",
|
|
45
|
+
"confidence": 0.8,
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
# Terraform Rules
|
|
49
|
+
{
|
|
50
|
+
"id": "IAC-TF-001",
|
|
51
|
+
"file_types": [".tf", ".hcl"],
|
|
52
|
+
"pattern": r'cidr_blocks\s*=\s*\[\s*["\']0\.0\.0\.0/0["\']\s*\]',
|
|
53
|
+
"title": "Security group open to the world (0.0.0.0/0)",
|
|
54
|
+
"description": "Security group ingress allows unrestricted inbound traffic from all IP addresses",
|
|
55
|
+
"severity": "critical",
|
|
56
|
+
"cwe": "CWE-284",
|
|
57
|
+
"confidence": 0.95,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "IAC-TF-002",
|
|
61
|
+
"file_types": [".tf", ".hcl"],
|
|
62
|
+
"pattern": r'acl\s*=\s*["\']public-read["\']|acl\s*=\s*["\']public-read-write["\']',
|
|
63
|
+
"title": "S3 bucket with public read/write ACL",
|
|
64
|
+
"description": "Cloud storage bucket configured with public access, risk of data exposure",
|
|
65
|
+
"severity": "critical",
|
|
66
|
+
"cwe": "CWE-732",
|
|
67
|
+
"confidence": 0.95,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"id": "IAC-TF-003",
|
|
71
|
+
"file_types": [".tf", ".hcl"],
|
|
72
|
+
"pattern": r'encrypted\s*=\s*false',
|
|
73
|
+
"title": "Unencrypted storage volume",
|
|
74
|
+
"description": "Cloud volume or database is configured without server-side encryption enabled",
|
|
75
|
+
"severity": "high",
|
|
76
|
+
"cwe": "CWE-311",
|
|
77
|
+
"confidence": 0.9,
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
# Kubernetes Rules
|
|
81
|
+
{
|
|
82
|
+
"id": "IAC-K8S-001",
|
|
83
|
+
"file_types": [".yaml", ".yml"],
|
|
84
|
+
"pattern": r'privileged:\s*true',
|
|
85
|
+
"title": "Kubernetes container running in privileged mode",
|
|
86
|
+
"description": "Privileged container allows root escalation and container breakout on the host node",
|
|
87
|
+
"severity": "critical",
|
|
88
|
+
"cwe": "CWE-250",
|
|
89
|
+
"confidence": 0.95,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"id": "IAC-K8S-002",
|
|
93
|
+
"file_types": [".yaml", ".yml"],
|
|
94
|
+
"pattern": r'hostNetwork:\s*true|hostPID:\s*true|hostIPC:\s*true',
|
|
95
|
+
"title": "Kubernetes pod sharing host namespace",
|
|
96
|
+
"description": "Sharing host network, PID, or IPC allows pod to snoop and interact with host system",
|
|
97
|
+
"severity": "high",
|
|
98
|
+
"cwe": "CWE-668",
|
|
99
|
+
"confidence": 0.9,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"id": "IAC-K8S-003",
|
|
103
|
+
"file_types": [".yaml", ".yml"],
|
|
104
|
+
"pattern": r'allowPrivilegeEscalation:\s*true',
|
|
105
|
+
"title": "Allow Privilege Escalation enabled in pod",
|
|
106
|
+
"description": "Container process can gain more privileges than its parent process",
|
|
107
|
+
"severity": "high",
|
|
108
|
+
"cwe": "CWE-250",
|
|
109
|
+
"confidence": 0.9,
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
# CI/CD Workflow Rules (GitHub Actions, GitLab CI)
|
|
113
|
+
{
|
|
114
|
+
"id": "IAC-CICD-001",
|
|
115
|
+
"file_types": [".yaml", ".yml"],
|
|
116
|
+
"pattern": r'pull_request_target:',
|
|
117
|
+
"title": "Dangerous pull_request_target trigger in CI workflow",
|
|
118
|
+
"description": "pull_request_target runs with write permissions and secrets access on untrusted PR fork code",
|
|
119
|
+
"severity": "critical",
|
|
120
|
+
"cwe": "CWE-829",
|
|
121
|
+
"confidence": 0.9,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"id": "IAC-CICD-002",
|
|
125
|
+
"file_types": [".yaml", ".yml"],
|
|
126
|
+
"pattern": r'uses:\s*[a-zA-Z0-9_-]+/[a-zA-Z0-9_.-]+@(main|master|latest)\b',
|
|
127
|
+
"title": "Unpinned GitHub Actions step reference",
|
|
128
|
+
"description": "Third-party action pinned to branch instead of full commit SHA hash, supply chain risk",
|
|
129
|
+
"severity": "medium",
|
|
130
|
+
"cwe": "CWE-1104",
|
|
131
|
+
"confidence": 0.85,
|
|
132
|
+
},
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class IacSecurityEngine:
|
|
137
|
+
"""100% custom, in-house developed Infrastructure-as-Code & Cloud configuration scanner."""
|
|
138
|
+
|
|
139
|
+
name = "iac_security"
|
|
140
|
+
version = "1.0.0"
|
|
141
|
+
|
|
142
|
+
def __init__(self):
|
|
143
|
+
self._compiled_rules = []
|
|
144
|
+
for r in IAC_RULES:
|
|
145
|
+
self._compiled_rules.append({
|
|
146
|
+
**r,
|
|
147
|
+
"regex": re.compile(r["pattern"], re.MULTILINE)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
async def check_availability(self) -> bool:
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
def resource_limits(self) -> ResourceLimits:
|
|
154
|
+
return ResourceLimits(max_cpu_seconds=30, max_memory_mb=256)
|
|
155
|
+
|
|
156
|
+
async def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
|
|
157
|
+
path_str = ir.metadata.get("path", "").lower()
|
|
158
|
+
ext = Path(path_str).suffix.lower()
|
|
159
|
+
filename = Path(path_str).name.lower()
|
|
160
|
+
|
|
161
|
+
# Check if file is relevant for IaC
|
|
162
|
+
is_relevant = (
|
|
163
|
+
ext in (".tf", ".hcl", ".dockerfile", ".yaml", ".yml", ".json") or
|
|
164
|
+
filename in ("dockerfile", "docker-compose.yml", "docker-compose.yaml") or
|
|
165
|
+
".github" in path_str or "k8s" in path_str or "terraform" in path_str
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if not is_relevant:
|
|
169
|
+
return EngineResult(engine=self.name, findings=[], ok=True)
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
content = ir.content.decode("utf-8", errors="replace")
|
|
173
|
+
except Exception:
|
|
174
|
+
return EngineResult(engine=self.name, findings=[], ok=True)
|
|
175
|
+
|
|
176
|
+
lines = content.splitlines()
|
|
177
|
+
findings = []
|
|
178
|
+
|
|
179
|
+
for rule in self._compiled_rules:
|
|
180
|
+
# Match file type
|
|
181
|
+
matches_file = any(
|
|
182
|
+
ft == ext or ft == filename or (ft.startswith(".") and path_str.endswith(ft))
|
|
183
|
+
for ft in rule["file_types"]
|
|
184
|
+
)
|
|
185
|
+
if not matches_file:
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
for match in rule["regex"].finditer(content):
|
|
189
|
+
line_no = content[:match.start()].count('\n') + 1
|
|
190
|
+
start_line = max(0, line_no - 2)
|
|
191
|
+
end_line = min(len(lines), line_no + 2)
|
|
192
|
+
snippet = '\n'.join(lines[start_line:end_line])
|
|
193
|
+
|
|
194
|
+
findings.append(Finding(
|
|
195
|
+
engine=self.name,
|
|
196
|
+
severity=rule["severity"],
|
|
197
|
+
title=f"{rule['id']}: {rule['title']}",
|
|
198
|
+
description=rule["description"],
|
|
199
|
+
location=f"{ir.metadata.get('path', 'unknown')}:{line_no}",
|
|
200
|
+
metadata={
|
|
201
|
+
"rule_id": rule["id"],
|
|
202
|
+
"cwe": rule["cwe"],
|
|
203
|
+
"confidence": rule["confidence"],
|
|
204
|
+
"snippet": snippet,
|
|
205
|
+
"file": ir.metadata.get("path", "unknown"),
|
|
206
|
+
"line": line_no,
|
|
207
|
+
}
|
|
208
|
+
))
|
|
209
|
+
|
|
210
|
+
return EngineResult(
|
|
211
|
+
engine=self.name,
|
|
212
|
+
findings=findings,
|
|
213
|
+
ok=True,
|
|
214
|
+
metrics={"findings_count": len(findings)}
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
Engine = IacSecurityEngine
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class OrionIR:
|
|
8
|
+
content: bytes
|
|
9
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
10
|
+
_hash: str = field(default="", repr=False)
|
|
11
|
+
|
|
12
|
+
def __post_init__(self):
|
|
13
|
+
if not self._hash:
|
|
14
|
+
self._hash = hashlib.sha256(self.content).hexdigest()[:16]
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def address(self) -> str:
|
|
18
|
+
return self._hash
|
|
19
|
+
|
|
20
|
+
def with_meta(self, **kwargs) -> "OrionIR":
|
|
21
|
+
meta = {**self.metadata, **kwargs}
|
|
22
|
+
return OrionIR(content=self.content, metadata=meta, _hash=self._hash)
|