@orionscanner/cli 0.1.0 → 0.5.1
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/README.md +80 -190
- package/bin/orion.js +30 -29
- package/dist/orion.exe +0 -0
- package/package.json +46 -48
- package/docs/API_REFERENCE.md +0 -211
- package/docs/ARCHITECTURE.md +0 -183
- package/docs/CHANGELOG.md +0 -112
- package/docs/CONTRIBUTING.md +0 -157
- package/docs/ENGINE_CATALOG.md +0 -87
- package/docs/GETTING_STARTED.md +0 -226
- package/pyproject.toml +0 -16
- package/src/orion/__init__.py +0 -0
- package/src/orion/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/orion/engine/__init__.py +0 -17
- 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 +0 -125
- package/src/orion/engine/architecture/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/architecture/engine.py +0 -463
- package/src/orion/engine/authorization/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/authorization/engine.py +0 -337
- package/src/orion/engine/callgraph/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/callgraph/engine.py +0 -236
- package/src/orion/engine/cfg/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/cfg/engine.py +0 -509
- package/src/orion/engine/chaos/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/chaos/engine.py +0 -268
- package/src/orion/engine/chaos_config/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/chaos_config/engine.py +0 -322
- package/src/orion/engine/complexity/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/complexity/engine.py +0 -144
- package/src/orion/engine/compliance/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/compliance/engine.py +0 -325
- package/src/orion/engine/config.py +0 -41
- package/src/orion/engine/config_engine/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/config_engine/engine.py +0 -364
- package/src/orion/engine/config_validation/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/config_validation/engine.py +0 -313
- package/src/orion/engine/container/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/container/engine.py +0 -546
- package/src/orion/engine/contract_probe/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/contract_probe/engine.py +0 -316
- package/src/orion/engine/core.py +0 -75
- package/src/orion/engine/crypto_guard/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/crypto_guard/engine.py +0 -134
- package/src/orion/engine/data_residency/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/data_residency/engine.py +0 -238
- package/src/orion/engine/dataflow/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/dataflow/engine.py +0 -398
- package/src/orion/engine/dead_import/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/dead_import/engine.py +0 -293
- package/src/orion/engine/deps/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/deps/engine.py +0 -244
- package/src/orion/engine/dispatcher.py +0 -350
- package/src/orion/engine/docs/AUDIT_PROMPT.md +0 -1248
- package/src/orion/engine/duplication/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/duplication/engine.py +0 -120
- package/src/orion/engine/error_handling/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/error_handling/engine.py +0 -256
- package/src/orion/engine/fp_learn/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/fp_learn/engine.py +0 -248
- package/src/orion/engine/hunter/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/hunter/engine.py +0 -324
- package/src/orion/engine/iac_security/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/iac_security/engine.py +0 -218
- package/src/orion/engine/ir.py +0 -22
- package/src/orion/engine/languages.py +0 -454
- package/src/orion/engine/license/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/license/engine.py +0 -172
- package/src/orion/engine/lint/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/lint/engine.py +0 -558
- package/src/orion/engine/llm_security/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/llm_security/engine.py +0 -125
- package/src/orion/engine/logging_c/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/logging_c/engine.py +0 -164
- package/src/orion/engine/noise/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/noise/engine.py +0 -140
- package/src/orion/engine/orphan/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/orphan/engine.py +0 -244
- package/src/orion/engine/perf_guard/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/perf_guard/engine.py +0 -345
- package/src/orion/engine/prompt_security/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/prompt_security/detectors/__init__.py +0 -4
- package/src/orion/engine/prompt_security/engine.py +0 -170
- package/src/orion/engine/provenance/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/provenance/engine.py +0 -242
- package/src/orion/engine/registry.py +0 -55
- package/src/orion/engine/regression/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/regression/engine.py +0 -213
- package/src/orion/engine/results.py +0 -21
- package/src/orion/engine/runner.py +0 -1310
- package/src/orion/engine/sandbox_escape/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/sandbox_escape/engine.py +0 -284
- package/src/orion/engine/sast/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/sast/engine.py +0 -217
- package/src/orion/engine/sast/patterns/__init__.py +0 -513
- 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 +0 -77
- package/src/orion/engine/secrets/patterns/__init__.py +0 -74
- 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 +0 -143
- package/src/orion/engine/state/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/state/engine.py +0 -218
- package/src/orion/engine/supply_chain_sbom/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/supply_chain_sbom/engine.py +0 -290
- package/src/orion/engine/symbol/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/symbol/engine.py +0 -463
- package/src/orion/engine/taint/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/taint/engine.py +0 -464
- package/src/orion/engine/test_coverage/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/test_coverage/engine.py +0 -445
- package/src/orion/engine/threat_model/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/threat_model/engine.py +0 -126
- package/src/orion/engine/wiring_trace/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/wiring_trace/engine.py +0 -200
- package/src/orion_engine.egg-info/PKG-INFO +0 -7
- package/src/orion_engine.egg-info/SOURCES.txt +0 -65
- package/src/orion_engine.egg-info/dependency_links.txt +0 -1
- package/src/orion_engine.egg-info/entry_points.txt +0 -2
- package/src/orion_engine.egg-info/top_level.txt +0 -1
|
@@ -1,546 +0,0 @@
|
|
|
1
|
-
"""Container/security config analysis engine - analyzes Dockerfiles, docker-compose, Kubernetes, Helm."""
|
|
2
|
-
import re
|
|
3
|
-
import yaml
|
|
4
|
-
import json
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Dict, List, Optional, Any, Set
|
|
7
|
-
from dataclasses import dataclass
|
|
8
|
-
|
|
9
|
-
from orion.engine.config import EngineConfig
|
|
10
|
-
from orion.engine.ir import OrionIR
|
|
11
|
-
from orion.engine.results import EngineResult, Finding
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@dataclass
|
|
15
|
-
class ContainerFinding:
|
|
16
|
-
severity: str
|
|
17
|
-
title: str
|
|
18
|
-
description: str
|
|
19
|
-
location: str
|
|
20
|
-
metadata: dict
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class ContainerEngine:
|
|
24
|
-
name = "container"
|
|
25
|
-
version = "1.0.0"
|
|
26
|
-
|
|
27
|
-
CONTAINER_FILES = {
|
|
28
|
-
"dockerfile": ["Dockerfile", "Dockerfile.*", "*.dockerfile"],
|
|
29
|
-
"docker_compose": ["docker-compose.yml", "docker-compose.yaml", "docker-compose.*.yml", "docker-compose.*.yaml"],
|
|
30
|
-
"dockerignore": [".dockerignore"],
|
|
31
|
-
"kubernetes": ["kubernetes/*.yaml", "kubernetes/*.yml", "k8s/*.yaml", "k8s/*.yml", "manifests/*.yaml", "manifests/*.yml"],
|
|
32
|
-
"helm": ["helm/**/values.yaml", "helm/**/values.yml", "charts/**/values.yaml", "charts/**/values.yml"],
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
SECRET_PATTERNS = [
|
|
36
|
-
(re.compile(r'(password|passwd|pwd)\s*[:=]\s*["\']?[^"\'\s]{8,}'), "password"),
|
|
37
|
-
(re.compile(r'(api[_-]?key|apikey|secret[_-]?key|access[_-]?token)\s*[:=]\s*["\']?[^"\'\s]{16,}'), "api_key"),
|
|
38
|
-
(re.compile(r'AKIA[0-9A-Z]{16}'), "aws_access_key"),
|
|
39
|
-
(re.compile(r'-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----'), "private_key"),
|
|
40
|
-
(re.compile(r'(jdbc|mongodb|redis|mysql|postgres)://[^:\s]+:[^@\s]+@'), "connection_string"),
|
|
41
|
-
]
|
|
42
|
-
|
|
43
|
-
async def check_availability(self) -> bool:
|
|
44
|
-
return True
|
|
45
|
-
|
|
46
|
-
def resource_limits(self):
|
|
47
|
-
from orion.engine.config import ResourceLimits
|
|
48
|
-
return ResourceLimits(max_cpu_seconds=30, max_memory_mb=128)
|
|
49
|
-
|
|
50
|
-
def _should_exclude(self, filepath: str, config: EngineConfig) -> bool:
|
|
51
|
-
path = Path(filepath)
|
|
52
|
-
if config.toggles.get("exclude"):
|
|
53
|
-
for pattern in config.toggles["exclude"]:
|
|
54
|
-
if path.match(pattern) or any(p.match(pattern) for p in path.parents):
|
|
55
|
-
return True
|
|
56
|
-
return False
|
|
57
|
-
|
|
58
|
-
def _detect_file_type(self, filepath: str) -> Optional[str]:
|
|
59
|
-
path = Path(filepath)
|
|
60
|
-
name = path.name.lower()
|
|
61
|
-
|
|
62
|
-
if name == "dockerfile" or name.startswith("dockerfile.") or name.endswith(".dockerfile"):
|
|
63
|
-
return "dockerfile"
|
|
64
|
-
if name.startswith("docker-compose") and (name.endswith(".yml") or name.endswith(".yaml")):
|
|
65
|
-
return "docker_compose"
|
|
66
|
-
if name == ".dockerignore":
|
|
67
|
-
return "dockerignore"
|
|
68
|
-
if "kubernetes" in path.parts or "k8s" in path.parts or "manifests" in path.parts:
|
|
69
|
-
if name.endswith((".yaml", ".yml")):
|
|
70
|
-
return "kubernetes"
|
|
71
|
-
if "helm" in path.parts or "charts" in path.parts:
|
|
72
|
-
if name in ("values.yaml", "values.yml"):
|
|
73
|
-
return "helm"
|
|
74
|
-
return None
|
|
75
|
-
|
|
76
|
-
def _scan_secrets(self, content: str, filepath: str) -> List[ContainerFinding]:
|
|
77
|
-
findings = []
|
|
78
|
-
lines = content.splitlines()
|
|
79
|
-
for i, line in enumerate(lines, 1):
|
|
80
|
-
for pattern, secret_type in self.SECRET_PATTERNS:
|
|
81
|
-
if pattern.search(line):
|
|
82
|
-
findings.append(ContainerFinding(
|
|
83
|
-
severity="critical",
|
|
84
|
-
title=f"Hardcoded {secret_type} in {Path(filepath).name}",
|
|
85
|
-
description=f"Potential {secret_type} found in container config",
|
|
86
|
-
location=f"{filepath}:{i}",
|
|
87
|
-
metadata={"rule": f"secret_{secret_type}", "file": filepath, "line": i, "type": secret_type},
|
|
88
|
-
))
|
|
89
|
-
return findings
|
|
90
|
-
|
|
91
|
-
def _analyze_dockerfile(self, content: str, filepath: str) -> List[ContainerFinding]:
|
|
92
|
-
findings = []
|
|
93
|
-
lines = content.splitlines()
|
|
94
|
-
has_user = False
|
|
95
|
-
has_healthcheck = False
|
|
96
|
-
from_latest = False
|
|
97
|
-
copy_dot = False
|
|
98
|
-
has_add_url = False
|
|
99
|
-
|
|
100
|
-
for i, line in enumerate(lines, 1):
|
|
101
|
-
stripped = line.strip()
|
|
102
|
-
upper = stripped.upper()
|
|
103
|
-
|
|
104
|
-
# Check for USER instruction
|
|
105
|
-
if upper.startswith("USER "):
|
|
106
|
-
user = stripped[5:].strip()
|
|
107
|
-
if user == "root" or user == "0":
|
|
108
|
-
findings.append(ContainerFinding(
|
|
109
|
-
severity="high",
|
|
110
|
-
title="Container runs as root",
|
|
111
|
-
description="Dockerfile specifies USER root - containers should run as non-root user",
|
|
112
|
-
location=f"{filepath}:{i}",
|
|
113
|
-
metadata={"rule": "user_root", "file": filepath, "line": i},
|
|
114
|
-
))
|
|
115
|
-
has_user = True
|
|
116
|
-
|
|
117
|
-
# Check for FROM latest or no tag
|
|
118
|
-
if upper.startswith("FROM "):
|
|
119
|
-
from_part = stripped[5:].strip()
|
|
120
|
-
if ":latest" in from_part or (":" not in from_part and "@" not in from_part and not from_part.startswith("--")):
|
|
121
|
-
findings.append(ContainerFinding(
|
|
122
|
-
severity="medium",
|
|
123
|
-
title="Base image uses 'latest' tag or no tag",
|
|
124
|
-
description="Using 'latest' or no tag makes builds non-reproducible",
|
|
125
|
-
location=f"{filepath}:{i}",
|
|
126
|
-
metadata={"rule": "from_latest", "file": filepath, "line": i, "image": from_part},
|
|
127
|
-
))
|
|
128
|
-
from_latest = True
|
|
129
|
-
|
|
130
|
-
# Check for COPY . .
|
|
131
|
-
if upper.startswith("COPY ") and ". ." in stripped:
|
|
132
|
-
findings.append(ContainerFinding(
|
|
133
|
-
severity="medium",
|
|
134
|
-
title="COPY . . copies entire build context",
|
|
135
|
-
description="COPY . . copies all files including secrets, tests, etc. - use explicit paths",
|
|
136
|
-
location=f"{filepath}:{i}",
|
|
137
|
-
metadata={"rule": "copy_dot_dot", "file": filepath, "line": i},
|
|
138
|
-
))
|
|
139
|
-
copy_dot = True
|
|
140
|
-
|
|
141
|
-
# Check for ADD from URL
|
|
142
|
-
if upper.startswith("ADD ") and ("http://" in stripped or "https://" in stripped):
|
|
143
|
-
findings.append(ContainerFinding(
|
|
144
|
-
severity="high",
|
|
145
|
-
title="ADD with remote URL",
|
|
146
|
-
description="ADD with URL can introduce supply chain risks - use COPY or RUN with explicit download",
|
|
147
|
-
location=f"{filepath}:{i}",
|
|
148
|
-
metadata={"rule": "add_remote_url", "file": filepath, "line": i},
|
|
149
|
-
))
|
|
150
|
-
has_add_url = True
|
|
151
|
-
|
|
152
|
-
# Check for secrets in ENV/ARG
|
|
153
|
-
if upper.startswith("ENV ") or upper.startswith("ARG "):
|
|
154
|
-
for pattern, secret_type in self.SECRET_PATTERNS:
|
|
155
|
-
if pattern.search(stripped):
|
|
156
|
-
findings.append(ContainerFinding(
|
|
157
|
-
severity="critical",
|
|
158
|
-
title=f"Secret in {upper.split()[0]} instruction",
|
|
159
|
-
description=f"Potential {secret_type} in {upper.split()[0]} - use build secrets or runtime env",
|
|
160
|
-
location=f"{filepath}:{i}",
|
|
161
|
-
metadata={"rule": f"secret_in_{upper.split()[0].lower()}", "file": filepath, "line": i, "type": secret_type},
|
|
162
|
-
))
|
|
163
|
-
|
|
164
|
-
# Check for HEALTHCHECK
|
|
165
|
-
if upper.startswith("HEALTHCHECK"):
|
|
166
|
-
has_healthcheck = True
|
|
167
|
-
|
|
168
|
-
# Missing USER
|
|
169
|
-
if not has_user:
|
|
170
|
-
findings.append(ContainerFinding(
|
|
171
|
-
severity="high",
|
|
172
|
-
title="Missing USER instruction",
|
|
173
|
-
description="Container will run as root by default - add USER instruction",
|
|
174
|
-
location=filepath,
|
|
175
|
-
metadata={"rule": "missing_user", "file": filepath},
|
|
176
|
-
))
|
|
177
|
-
|
|
178
|
-
# Missing HEALTHCHECK
|
|
179
|
-
if not has_healthcheck:
|
|
180
|
-
findings.append(ContainerFinding(
|
|
181
|
-
severity="medium",
|
|
182
|
-
title="Missing HEALTHCHECK",
|
|
183
|
-
description="No health check defined - container orchestration cannot verify health",
|
|
184
|
-
location=filepath,
|
|
185
|
-
metadata={"rule": "missing_healthcheck", "file": filepath},
|
|
186
|
-
))
|
|
187
|
-
|
|
188
|
-
return findings
|
|
189
|
-
|
|
190
|
-
def _analyze_docker_compose(self, content: str, filepath: str) -> List[ContainerFinding]:
|
|
191
|
-
findings = []
|
|
192
|
-
try:
|
|
193
|
-
compose = yaml.safe_load(content)
|
|
194
|
-
if not compose or "services" not in compose:
|
|
195
|
-
return findings
|
|
196
|
-
|
|
197
|
-
for service_name, service_config in compose.get("services", {}).items():
|
|
198
|
-
if not isinstance(service_config, dict):
|
|
199
|
-
continue
|
|
200
|
-
|
|
201
|
-
# privileged
|
|
202
|
-
if service_config.get("privileged"):
|
|
203
|
-
findings.append(ContainerFinding(
|
|
204
|
-
severity="critical",
|
|
205
|
-
title=f"Service '{service_name}' runs in privileged mode",
|
|
206
|
-
description="Privileged containers have full access to host - major security risk",
|
|
207
|
-
location=f"{filepath}:services.{service_name}.privileged",
|
|
208
|
-
metadata={"rule": "privileged", "file": filepath, "service": service_name},
|
|
209
|
-
))
|
|
210
|
-
|
|
211
|
-
# cap_add: ALL
|
|
212
|
-
cap_add = service_config.get("cap_add", [])
|
|
213
|
-
if isinstance(cap_add, list) and "ALL" in cap_add:
|
|
214
|
-
findings.append(ContainerFinding(
|
|
215
|
-
severity="high",
|
|
216
|
-
title=f"Service '{service_name}' adds ALL capabilities",
|
|
217
|
-
description="Adding ALL capabilities defeats container isolation",
|
|
218
|
-
location=f"{filepath}:services.{service_name}.cap_add",
|
|
219
|
-
metadata={"rule": "cap_add_all", "file": filepath, "service": service_name},
|
|
220
|
-
))
|
|
221
|
-
|
|
222
|
-
# network_mode: host
|
|
223
|
-
if service_config.get("network_mode") == "host":
|
|
224
|
-
findings.append(ContainerFinding(
|
|
225
|
-
severity="high",
|
|
226
|
-
title=f"Service '{service_name}' uses host network",
|
|
227
|
-
description="Host network mode gives container full access to host network stack",
|
|
228
|
-
location=f"{filepath}:services.{service_name}.network_mode",
|
|
229
|
-
metadata={"rule": "network_mode_host", "file": filepath, "service": service_name},
|
|
230
|
-
))
|
|
231
|
-
|
|
232
|
-
# pid_mode: host
|
|
233
|
-
if service_config.get("pid_mode") == "host":
|
|
234
|
-
findings.append(ContainerFinding(
|
|
235
|
-
severity="high",
|
|
236
|
-
title=f"Service '{service_name}' uses host PID namespace",
|
|
237
|
-
description="Host PID mode allows container to see all host processes",
|
|
238
|
-
location=f"{filepath}:services.{service_name}.pid_mode",
|
|
239
|
-
metadata={"rule": "pid_mode_host", "file": filepath, "service": service_name},
|
|
240
|
-
))
|
|
241
|
-
|
|
242
|
-
# ipc_mode: host
|
|
243
|
-
if service_config.get("ipc_mode") == "host":
|
|
244
|
-
findings.append(ContainerFinding(
|
|
245
|
-
severity="high",
|
|
246
|
-
title=f"Service '{service_name}' uses host IPC namespace",
|
|
247
|
-
description="Host IPC mode allows container to share IPC with host",
|
|
248
|
-
location=f"{filepath}:services.{service_name}.ipc_mode",
|
|
249
|
-
metadata={"rule": "ipc_mode_host", "file": filepath, "service": service_name},
|
|
250
|
-
))
|
|
251
|
-
|
|
252
|
-
# read_only
|
|
253
|
-
if not service_config.get("read_only"):
|
|
254
|
-
findings.append(ContainerFinding(
|
|
255
|
-
severity="medium",
|
|
256
|
-
title=f"Service '{service_name}' missing read_only: true",
|
|
257
|
-
description="Container filesystem should be read-only for security",
|
|
258
|
-
location=f"{filepath}:services.{service_name}.read_only",
|
|
259
|
-
metadata={"rule": "missing_read_only", "file": filepath, "service": service_name},
|
|
260
|
-
))
|
|
261
|
-
|
|
262
|
-
# resource limits
|
|
263
|
-
deploy = service_config.get("deploy", {})
|
|
264
|
-
resources = deploy.get("resources", {})
|
|
265
|
-
limits = resources.get("limits", {})
|
|
266
|
-
if not limits:
|
|
267
|
-
findings.append(ContainerFinding(
|
|
268
|
-
severity="medium",
|
|
269
|
-
title=f"Service '{service_name}' missing resource limits",
|
|
270
|
-
description="Containers should have CPU/memory limits to prevent DoS",
|
|
271
|
-
location=f"{filepath}:services.{service_name}.deploy.resources.limits",
|
|
272
|
-
metadata={"rule": "missing_resource_limits", "file": filepath, "service": service_name},
|
|
273
|
-
))
|
|
274
|
-
|
|
275
|
-
# secrets in environment
|
|
276
|
-
env = service_config.get("environment", {})
|
|
277
|
-
if isinstance(env, dict):
|
|
278
|
-
for key, value in env.items():
|
|
279
|
-
for pattern, secret_type in self.SECRET_PATTERNS:
|
|
280
|
-
if isinstance(value, str) and pattern.search(value):
|
|
281
|
-
findings.append(ContainerFinding(
|
|
282
|
-
severity="critical",
|
|
283
|
-
title=f"Secret in environment variable for service '{service_name}'",
|
|
284
|
-
description=f"Potential {secret_type} in environment variable '{key}'",
|
|
285
|
-
location=f"{filepath}:services.{service_name}.environment.{key}",
|
|
286
|
-
metadata={"rule": f"secret_in_env", "file": filepath, "service": service_name, "env_var": key, "type": secret_type},
|
|
287
|
-
))
|
|
288
|
-
|
|
289
|
-
except yaml.YAMLError:
|
|
290
|
-
pass
|
|
291
|
-
return findings
|
|
292
|
-
|
|
293
|
-
def _analyze_kubernetes(self, content: str, filepath: str) -> List[ContainerFinding]:
|
|
294
|
-
findings = []
|
|
295
|
-
try:
|
|
296
|
-
docs = list(yaml.safe_load_all(content))
|
|
297
|
-
for doc in docs:
|
|
298
|
-
if not doc or not isinstance(doc, dict):
|
|
299
|
-
continue
|
|
300
|
-
|
|
301
|
-
kind = doc.get("kind", "")
|
|
302
|
-
metadata = doc.get("metadata", {})
|
|
303
|
-
name = metadata.get("name", "unknown")
|
|
304
|
-
namespace = metadata.get("namespace", "default")
|
|
305
|
-
|
|
306
|
-
# Check Pod security context
|
|
307
|
-
spec = doc.get("spec", {})
|
|
308
|
-
template_spec = spec.get("template", {}).get("spec", spec)
|
|
309
|
-
|
|
310
|
-
# allowPrivilegeEscalation
|
|
311
|
-
containers = template_spec.get("containers", [])
|
|
312
|
-
for container in containers:
|
|
313
|
-
if not isinstance(container, dict):
|
|
314
|
-
continue
|
|
315
|
-
container_name = container.get("name", "unknown")
|
|
316
|
-
security_context = container.get("securityContext", {})
|
|
317
|
-
|
|
318
|
-
if security_context.get("allowPrivilegeEscalation") is True:
|
|
319
|
-
findings.append(ContainerFinding(
|
|
320
|
-
severity="high",
|
|
321
|
-
title=f"Container '{container_name}' allows privilege escalation",
|
|
322
|
-
description="allowPrivilegeEscalation: true allows processes to gain more privileges",
|
|
323
|
-
location=f"{filepath}:{kind}/{name}.spec.containers[{container_name}].securityContext.allowPrivilegeEscalation",
|
|
324
|
-
metadata={"rule": "allow_privilege_escalation", "file": filepath, "kind": kind, "name": name, "container": container_name},
|
|
325
|
-
))
|
|
326
|
-
|
|
327
|
-
if security_context.get("runAsRoot") is True:
|
|
328
|
-
findings.append(ContainerFinding(
|
|
329
|
-
severity="high",
|
|
330
|
-
title=f"Container '{container_name}' runs as root",
|
|
331
|
-
description="runAsRoot: true allows container to run as root user",
|
|
332
|
-
location=f"{filepath}:{kind}/{name}.spec.containers[{container_name}].securityContext.runAsRoot",
|
|
333
|
-
metadata={"rule": "run_as_root", "file": filepath, "kind": kind, "name": name, "container": container_name},
|
|
334
|
-
))
|
|
335
|
-
|
|
336
|
-
if security_context.get("privileged") is True:
|
|
337
|
-
findings.append(ContainerFinding(
|
|
338
|
-
severity="critical",
|
|
339
|
-
title=f"Container '{container_name}' is privileged",
|
|
340
|
-
description="Privileged containers have full access to host",
|
|
341
|
-
location=f"{filepath}:{kind}/{name}.spec.containers[{container_name}].securityContext.privileged",
|
|
342
|
-
metadata={"rule": "privileged_container", "file": filepath, "kind": kind, "name": name, "container": container_name},
|
|
343
|
-
))
|
|
344
|
-
|
|
345
|
-
# capabilities
|
|
346
|
-
caps = security_context.get("capabilities", {})
|
|
347
|
-
if isinstance(caps, dict):
|
|
348
|
-
add_caps = caps.get("add", [])
|
|
349
|
-
if "ALL" in add_caps:
|
|
350
|
-
findings.append(ContainerFinding(
|
|
351
|
-
severity="high",
|
|
352
|
-
title=f"Container '{container_name}' adds ALL capabilities",
|
|
353
|
-
description="Adding ALL capabilities defeats container isolation",
|
|
354
|
-
location=f"{filepath}:{kind}/{name}.spec.containers[{container_name}].securityContext.capabilities.add",
|
|
355
|
-
metadata={"rule": "cap_add_all_k8s", "file": filepath, "kind": kind, "name": name, "container": container_name},
|
|
356
|
-
))
|
|
357
|
-
|
|
358
|
-
# Host network/PID/IPC
|
|
359
|
-
if template_spec.get("hostNetwork"):
|
|
360
|
-
findings.append(ContainerFinding(
|
|
361
|
-
severity="high",
|
|
362
|
-
title=f"{kind} '{name}' uses hostNetwork",
|
|
363
|
-
description="hostNetwork: true gives pod access to host network stack",
|
|
364
|
-
location=f"{filepath}:{kind}/{name}.spec.hostNetwork",
|
|
365
|
-
metadata={"rule": "host_network", "file": filepath, "kind": kind, "name": name},
|
|
366
|
-
))
|
|
367
|
-
|
|
368
|
-
if template_spec.get("hostPID"):
|
|
369
|
-
findings.append(ContainerFinding(
|
|
370
|
-
severity="high",
|
|
371
|
-
title=f"{kind} '{name}' uses hostPID",
|
|
372
|
-
description="hostPID: true gives pod access to host process namespace",
|
|
373
|
-
location=f"{filepath}:{kind}/{name}.spec.hostPID",
|
|
374
|
-
metadata={"rule": "host_pid", "file": filepath, "kind": kind, "name": name},
|
|
375
|
-
))
|
|
376
|
-
|
|
377
|
-
if template_spec.get("hostIPC"):
|
|
378
|
-
findings.append(ContainerFinding(
|
|
379
|
-
severity="high",
|
|
380
|
-
title=f"{kind} '{name}' uses hostIPC",
|
|
381
|
-
description="hostIPC: true gives pod access to host IPC namespace",
|
|
382
|
-
location=f"{filepath}:{kind}/{name}.spec.hostIPC",
|
|
383
|
-
metadata={"rule": "host_ipc", "file": filepath, "kind": kind, "name": name},
|
|
384
|
-
))
|
|
385
|
-
|
|
386
|
-
# Resource limits
|
|
387
|
-
for container in containers:
|
|
388
|
-
resources = container.get("resources", {})
|
|
389
|
-
limits = resources.get("limits", {})
|
|
390
|
-
if not limits:
|
|
391
|
-
findings.append(ContainerFinding(
|
|
392
|
-
severity="medium",
|
|
393
|
-
title=f"Container '{container.get('name', 'unknown')}' missing resource limits",
|
|
394
|
-
description="Containers should have CPU/memory limits",
|
|
395
|
-
location=f"{filepath}:{kind}/{name}.spec.containers[{container.get('name', 'unknown')}].resources.limits",
|
|
396
|
-
metadata={"rule": "missing_resource_limits_k8s", "file": filepath, "kind": kind, "name": name, "container": container.get("name")},
|
|
397
|
-
))
|
|
398
|
-
|
|
399
|
-
# Secrets in configmaps/secrets (check for hardcoded values)
|
|
400
|
-
if kind in ("ConfigMap", "Secret"):
|
|
401
|
-
data = doc.get("data", {})
|
|
402
|
-
string_data = doc.get("stringData", {})
|
|
403
|
-
all_data = {**data, **string_data}
|
|
404
|
-
for key, value in all_data.items():
|
|
405
|
-
if isinstance(value, str):
|
|
406
|
-
for pattern, secret_type in self.SECRET_PATTERNS:
|
|
407
|
-
if pattern.search(value):
|
|
408
|
-
findings.append(ContainerFinding(
|
|
409
|
-
severity="critical" if kind == "Secret" else "high",
|
|
410
|
-
title=f"Hardcoded {secret_type} in {kind} '{name}'",
|
|
411
|
-
description=f"Potential {secret_type} in {kind} data key '{key}'",
|
|
412
|
-
location=f"{filepath}:{kind}/{name}.data.{key}",
|
|
413
|
-
metadata={"rule": f"secret_in_{kind.lower()}", "file": filepath, "kind": kind, "name": name, "key": key, "type": secret_type},
|
|
414
|
-
))
|
|
415
|
-
|
|
416
|
-
except yaml.YAMLError:
|
|
417
|
-
pass
|
|
418
|
-
return findings
|
|
419
|
-
|
|
420
|
-
def _analyze_helm_values(self, content: str, filepath: str) -> List[ContainerFinding]:
|
|
421
|
-
findings = []
|
|
422
|
-
try:
|
|
423
|
-
values = yaml.safe_load(content)
|
|
424
|
-
if not values or not isinstance(values, dict):
|
|
425
|
-
return findings
|
|
426
|
-
|
|
427
|
-
# Check for hardcoded secrets in values
|
|
428
|
-
def check_dict(d: dict, prefix: str = ""):
|
|
429
|
-
for key, value in d.items():
|
|
430
|
-
full_key = f"{prefix}.{key}" if prefix else key
|
|
431
|
-
if isinstance(value, str):
|
|
432
|
-
for pattern, secret_type in self.SECRET_PATTERNS:
|
|
433
|
-
if pattern.search(value):
|
|
434
|
-
findings.append(ContainerFinding(
|
|
435
|
-
severity="critical",
|
|
436
|
-
title=f"Hardcoded {secret_type} in Helm values",
|
|
437
|
-
description=f"Potential {secret_type} in values.{full_key}",
|
|
438
|
-
location=f"{filepath}:{full_key}",
|
|
439
|
-
metadata={"rule": "secret_in_helm_values", "file": filepath, "key": full_key, "type": secret_type},
|
|
440
|
-
))
|
|
441
|
-
elif isinstance(value, dict):
|
|
442
|
-
check_dict(value, full_key)
|
|
443
|
-
|
|
444
|
-
check_dict(values)
|
|
445
|
-
|
|
446
|
-
except yaml.YAMLError:
|
|
447
|
-
pass
|
|
448
|
-
return findings
|
|
449
|
-
|
|
450
|
-
def _analyze_dockerignore(self, content: str, filepath: str) -> List[ContainerFinding]:
|
|
451
|
-
findings = []
|
|
452
|
-
lines = content.splitlines()
|
|
453
|
-
has_git = False
|
|
454
|
-
has_env = False
|
|
455
|
-
has_secrets = False
|
|
456
|
-
|
|
457
|
-
for line in lines:
|
|
458
|
-
stripped = line.strip()
|
|
459
|
-
if not stripped or stripped.startswith("#"):
|
|
460
|
-
continue
|
|
461
|
-
if ".git" in stripped:
|
|
462
|
-
has_git = True
|
|
463
|
-
if ".env" in stripped or "*.env" in stripped:
|
|
464
|
-
has_env = True
|
|
465
|
-
if any(s in stripped.lower() for s in ["secret", "key", "password", "token", "credential"]):
|
|
466
|
-
has_secrets = True
|
|
467
|
-
|
|
468
|
-
if not has_git:
|
|
469
|
-
findings.append(ContainerFinding(
|
|
470
|
-
severity="low",
|
|
471
|
-
title=".dockerignore missing .git exclusion",
|
|
472
|
-
description="Should exclude .git directory from build context",
|
|
473
|
-
location=filepath,
|
|
474
|
-
metadata={"rule": "dockerignore_missing_git", "file": filepath},
|
|
475
|
-
))
|
|
476
|
-
|
|
477
|
-
if not has_env:
|
|
478
|
-
findings.append(ContainerFinding(
|
|
479
|
-
severity="medium",
|
|
480
|
-
title=".dockerignore missing .env exclusion",
|
|
481
|
-
description="Should exclude .env files from build context to prevent secret leakage",
|
|
482
|
-
location=filepath,
|
|
483
|
-
metadata={"rule": "dockerignore_missing_env", "file": filepath},
|
|
484
|
-
))
|
|
485
|
-
|
|
486
|
-
return findings
|
|
487
|
-
|
|
488
|
-
async def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
|
|
489
|
-
filepath = ir.metadata.get("path", "unknown")
|
|
490
|
-
|
|
491
|
-
if self._should_exclude(filepath, config):
|
|
492
|
-
return EngineResult(engine=self.name, ok=True, metrics={"skipped": "excluded"})
|
|
493
|
-
|
|
494
|
-
file_type = self._detect_file_type(filepath)
|
|
495
|
-
if not file_type:
|
|
496
|
-
return EngineResult(engine=self.name, ok=True, metrics={"skipped": "not_container_file"})
|
|
497
|
-
|
|
498
|
-
try:
|
|
499
|
-
content = ir.content.decode("utf-8", errors="replace")
|
|
500
|
-
except Exception:
|
|
501
|
-
content = str(ir.content)
|
|
502
|
-
|
|
503
|
-
all_findings = []
|
|
504
|
-
|
|
505
|
-
# Scan for secrets in all file types
|
|
506
|
-
all_findings.extend(self._scan_secrets(content, filepath))
|
|
507
|
-
|
|
508
|
-
# Type-specific analysis
|
|
509
|
-
if file_type == "dockerfile":
|
|
510
|
-
all_findings.extend(self._analyze_dockerfile(content, filepath))
|
|
511
|
-
elif file_type == "docker_compose":
|
|
512
|
-
all_findings.extend(self._analyze_docker_compose(content, filepath))
|
|
513
|
-
elif file_type == "kubernetes":
|
|
514
|
-
all_findings.extend(self._analyze_kubernetes(content, filepath))
|
|
515
|
-
elif file_type == "helm":
|
|
516
|
-
all_findings.extend(self._analyze_helm_values(content, filepath))
|
|
517
|
-
elif file_type == "dockerignore":
|
|
518
|
-
all_findings.extend(self._analyze_dockerignore(content, filepath))
|
|
519
|
-
|
|
520
|
-
# Convert to Finding objects
|
|
521
|
-
findings = []
|
|
522
|
-
for f in all_findings:
|
|
523
|
-
# Only keep findings with confidence >= 0.85 (as per requirements)
|
|
524
|
-
# All our rules have high confidence
|
|
525
|
-
findings.append(Finding(
|
|
526
|
-
engine=self.name,
|
|
527
|
-
severity=f.severity,
|
|
528
|
-
title=f.title,
|
|
529
|
-
description=f.description,
|
|
530
|
-
location=f.location,
|
|
531
|
-
metadata=f.metadata,
|
|
532
|
-
))
|
|
533
|
-
|
|
534
|
-
return EngineResult(
|
|
535
|
-
engine=self.name,
|
|
536
|
-
findings=findings,
|
|
537
|
-
ok=True,
|
|
538
|
-
metrics={
|
|
539
|
-
"file_type": file_type,
|
|
540
|
-
"file": filepath,
|
|
541
|
-
"findings_count": len(findings),
|
|
542
|
-
},
|
|
543
|
-
)
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
Engine = ContainerEngine
|
|
Binary file
|