@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,242 @@
|
|
|
1
|
+
"""Provenance attestation engine - generates SLSA-like provenance metadata."""
|
|
2
|
+
import subprocess
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, List, Optional, Any
|
|
9
|
+
from dataclasses import dataclass, asdict
|
|
10
|
+
|
|
11
|
+
from orion.engine.config import EngineConfig
|
|
12
|
+
from orion.engine.ir import OrionIR
|
|
13
|
+
from orion.engine.results import EngineResult, Finding
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ProvenanceData:
|
|
18
|
+
build_timestamp: str
|
|
19
|
+
git_commit_sha: Optional[str] = None
|
|
20
|
+
git_tag: Optional[str] = None
|
|
21
|
+
git_branch: Optional[str] = None
|
|
22
|
+
builder_identity: Optional[str] = None
|
|
23
|
+
file_hashes: Dict[str, str] = None
|
|
24
|
+
dependencies: Dict[str, str] = None
|
|
25
|
+
environment: Dict[str, str] = None
|
|
26
|
+
|
|
27
|
+
def __post_init__(self):
|
|
28
|
+
if self.file_hashes is None:
|
|
29
|
+
self.file_hashes = {}
|
|
30
|
+
if self.dependencies is None:
|
|
31
|
+
self.dependencies = {}
|
|
32
|
+
if self.environment is None:
|
|
33
|
+
self.environment = {}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ProvenanceEngine:
|
|
37
|
+
name = "provenance"
|
|
38
|
+
version = "1.0.0"
|
|
39
|
+
|
|
40
|
+
CI_ENV_VARS = {
|
|
41
|
+
"github": "GITHUB_SHA",
|
|
42
|
+
"gitlab": "GITLAB_COMMIT_SHA",
|
|
43
|
+
"buildkite": "BUILDKITE_COMMIT",
|
|
44
|
+
"circleci": "CIRCLE_SHA1",
|
|
45
|
+
"travis": "TRAVIS_COMMIT",
|
|
46
|
+
"jenkins": "GIT_COMMIT",
|
|
47
|
+
"azure": "BUILD_SOURCEVERSION",
|
|
48
|
+
"bitbucket": "BITBUCKET_COMMIT",
|
|
49
|
+
"drone": "DRONE_COMMIT_SHA",
|
|
50
|
+
"codebuild": "CODEBUILD_RESOLVED_SOURCE_VERSION",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async def check_availability(self) -> bool:
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
def resource_limits(self):
|
|
57
|
+
from orion.engine.config import ResourceLimits
|
|
58
|
+
return ResourceLimits(max_cpu_seconds=30, max_memory_mb=128)
|
|
59
|
+
|
|
60
|
+
def _get_git_info(self, project_root: str) -> Dict[str, Optional[str]]:
|
|
61
|
+
"""Extract git information from the project."""
|
|
62
|
+
info = {"commit_sha": None, "tag": None, "branch": None}
|
|
63
|
+
try:
|
|
64
|
+
# Get commit SHA
|
|
65
|
+
result = subprocess.run(
|
|
66
|
+
["git", "rev-parse", "HEAD"],
|
|
67
|
+
cwd=project_root,
|
|
68
|
+
capture_output=True,
|
|
69
|
+
text=True,
|
|
70
|
+
timeout=5,
|
|
71
|
+
)
|
|
72
|
+
if result.returncode == 0:
|
|
73
|
+
info["commit_sha"] = result.stdout.strip()
|
|
74
|
+
|
|
75
|
+
# Get tag
|
|
76
|
+
result = subprocess.run(
|
|
77
|
+
["git", "describe", "--tags", "--exact-match"],
|
|
78
|
+
cwd=project_root,
|
|
79
|
+
capture_output=True,
|
|
80
|
+
text=True,
|
|
81
|
+
timeout=5,
|
|
82
|
+
)
|
|
83
|
+
if result.returncode == 0:
|
|
84
|
+
info["tag"] = result.stdout.strip()
|
|
85
|
+
|
|
86
|
+
# Get branch
|
|
87
|
+
result = subprocess.run(
|
|
88
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
89
|
+
cwd=project_root,
|
|
90
|
+
capture_output=True,
|
|
91
|
+
text=True,
|
|
92
|
+
timeout=5,
|
|
93
|
+
)
|
|
94
|
+
if result.returncode == 0:
|
|
95
|
+
info["branch"] = result.stdout.strip()
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
return info
|
|
99
|
+
|
|
100
|
+
def _get_builder_identity(self) -> Optional[str]:
|
|
101
|
+
"""Detect CI environment and get builder identity."""
|
|
102
|
+
for ci_name, env_var in self.CI_ENV_VARS.items():
|
|
103
|
+
value = os.environ.get(env_var)
|
|
104
|
+
if value:
|
|
105
|
+
return f"{ci_name}:{value}"
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
def _calculate_file_hashes(self, all_files: Dict[str, OrionIR]) -> Dict[str, str]:
|
|
109
|
+
"""Calculate SHA256 hashes for all source files."""
|
|
110
|
+
hashes = {}
|
|
111
|
+
for filepath, file_ir in all_files.items():
|
|
112
|
+
try:
|
|
113
|
+
# Only hash source files (not binary, not generated)
|
|
114
|
+
path = Path(filepath)
|
|
115
|
+
if path.suffix in (".py", ".js", ".ts", ".java", ".go", ".rs", ".rb", ".php",
|
|
116
|
+
".cs", ".cpp", ".c", ".h", ".hpp", ".sh", ".yaml", ".yml",
|
|
117
|
+
".json", ".toml", ".xml", ".html", ".css", ".sql", ".md",
|
|
118
|
+
".txt", ".cfg", ".ini", ".conf", ".dockerfile", ".gitignore"):
|
|
119
|
+
content = file_ir.content
|
|
120
|
+
if isinstance(content, str):
|
|
121
|
+
content = content.encode("utf-8")
|
|
122
|
+
sha256 = hashlib.sha256(content).hexdigest()
|
|
123
|
+
hashes[filepath] = sha256
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
126
|
+
return hashes
|
|
127
|
+
|
|
128
|
+
def _get_dependencies(self, all_files: Dict[str, OrionIR], project_root: str) -> Dict[str, str]:
|
|
129
|
+
"""Extract dependencies with hashes from lock files."""
|
|
130
|
+
deps = {}
|
|
131
|
+
|
|
132
|
+
# Python dependencies
|
|
133
|
+
lock_files = [
|
|
134
|
+
"requirements.txt", "requirements.lock", "Pipfile.lock",
|
|
135
|
+
"poetry.lock", "pyproject.toml", "setup.py", "setup.cfg",
|
|
136
|
+
]
|
|
137
|
+
for lock_file in lock_files:
|
|
138
|
+
path = Path(project_root) / lock_file
|
|
139
|
+
if path.exists():
|
|
140
|
+
try:
|
|
141
|
+
content = path.read_bytes()
|
|
142
|
+
deps[lock_file] = hashlib.sha256(content).hexdigest()
|
|
143
|
+
except Exception:
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
# Node.js dependencies
|
|
147
|
+
for lock_file in ["package-lock.json", "yarn.lock", "pnpm-lock.yaml"]:
|
|
148
|
+
path = Path(project_root) / lock_file
|
|
149
|
+
if path.exists():
|
|
150
|
+
try:
|
|
151
|
+
content = path.read_bytes()
|
|
152
|
+
deps[lock_file] = hashlib.sha256(content).hexdigest()
|
|
153
|
+
except Exception:
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
# Go dependencies
|
|
157
|
+
for lock_file in ["go.sum", "go.mod"]:
|
|
158
|
+
path = Path(project_root) / lock_file
|
|
159
|
+
if path.exists():
|
|
160
|
+
try:
|
|
161
|
+
content = path.read_bytes()
|
|
162
|
+
deps[lock_file] = hashlib.sha256(content).hexdigest()
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
# Rust dependencies
|
|
167
|
+
path = Path(project_root) / "Cargo.lock"
|
|
168
|
+
if path.exists():
|
|
169
|
+
try:
|
|
170
|
+
content = path.read_bytes()
|
|
171
|
+
deps["Cargo.lock"] = hashlib.sha256(content).hexdigest()
|
|
172
|
+
except Exception:
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
# Java dependencies
|
|
176
|
+
for lock_file in ["pom.xml", "build.gradle", "build.gradle.kts"]:
|
|
177
|
+
path = Path(project_root) / lock_file
|
|
178
|
+
if path.exists():
|
|
179
|
+
try:
|
|
180
|
+
content = path.read_bytes()
|
|
181
|
+
deps[lock_file] = hashlib.sha256(content).hexdigest()
|
|
182
|
+
except Exception:
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
return deps
|
|
186
|
+
|
|
187
|
+
def _get_environment(self) -> Dict[str, str]:
|
|
188
|
+
"""Capture relevant environment variables."""
|
|
189
|
+
env = {}
|
|
190
|
+
relevant_vars = [
|
|
191
|
+
"CI", "GITHUB_ACTIONS", "GITLAB_CI", "BUILDKITE", "CIRCLECI",
|
|
192
|
+
"TRAVIS", "JENKINS_URL", "AZURE_PIPELINES", "BITBUCKET_BUILD_NUMBER",
|
|
193
|
+
"DRONE", "CODEBUILD_BUILD_ID", "PYTHON_VERSION", "NODE_VERSION",
|
|
194
|
+
"GO_VERSION", "RUST_VERSION", "JAVA_VERSION",
|
|
195
|
+
]
|
|
196
|
+
for var in relevant_vars:
|
|
197
|
+
value = os.environ.get(var)
|
|
198
|
+
if value:
|
|
199
|
+
env[var] = value
|
|
200
|
+
return env
|
|
201
|
+
|
|
202
|
+
async def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
|
|
203
|
+
project_root = ir.metadata.get("project_root", ".")
|
|
204
|
+
all_files = ir.metadata.get("all_files", {})
|
|
205
|
+
|
|
206
|
+
if not all_files:
|
|
207
|
+
all_files = {ir.metadata.get("path", "unknown"): ir}
|
|
208
|
+
|
|
209
|
+
# Gather provenance data
|
|
210
|
+
git_info = self._get_git_info(project_root)
|
|
211
|
+
builder_identity = self._get_builder_identity()
|
|
212
|
+
file_hashes = self._calculate_file_hashes(all_files)
|
|
213
|
+
dependencies = self._get_dependencies(all_files, project_root)
|
|
214
|
+
environment = self._get_environment()
|
|
215
|
+
|
|
216
|
+
provenance = ProvenanceData(
|
|
217
|
+
build_timestamp=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
218
|
+
git_commit_sha=git_info["commit_sha"],
|
|
219
|
+
git_tag=git_info["tag"],
|
|
220
|
+
git_branch=git_info["branch"],
|
|
221
|
+
builder_identity=builder_identity,
|
|
222
|
+
file_hashes=file_hashes,
|
|
223
|
+
dependencies=dependencies,
|
|
224
|
+
environment=environment,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Store in metrics (no findings for provenance engine)
|
|
228
|
+
return EngineResult(
|
|
229
|
+
engine=self.name,
|
|
230
|
+
findings=[],
|
|
231
|
+
ok=True,
|
|
232
|
+
metrics={
|
|
233
|
+
"provenance": asdict(provenance),
|
|
234
|
+
"files_hashed": len(file_hashes),
|
|
235
|
+
"dependency_files": len(dependencies),
|
|
236
|
+
"git_info_available": git_info["commit_sha"] is not None,
|
|
237
|
+
"ci_detected": builder_identity is not None,
|
|
238
|
+
},
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
Engine = ProvenanceEngine
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from typing import Dict, Type
|
|
2
|
+
import importlib
|
|
3
|
+
import pkgutil
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EngineRegistry:
|
|
8
|
+
_instance = None
|
|
9
|
+
|
|
10
|
+
def __new__(cls):
|
|
11
|
+
if cls._instance is None:
|
|
12
|
+
cls._instance = super().__new__(cls)
|
|
13
|
+
cls._instance._engines: Dict[str, Type] = {}
|
|
14
|
+
cls._instance._discovered = False
|
|
15
|
+
return cls._instance
|
|
16
|
+
|
|
17
|
+
def register(self, name: str, cls):
|
|
18
|
+
self._engines[name] = cls
|
|
19
|
+
|
|
20
|
+
def get(self, name: str):
|
|
21
|
+
return self._engines.get(name)
|
|
22
|
+
|
|
23
|
+
def all(self):
|
|
24
|
+
if not self._discovered:
|
|
25
|
+
self._discover_engines()
|
|
26
|
+
return dict(self._engines)
|
|
27
|
+
|
|
28
|
+
def _discover_engines(self):
|
|
29
|
+
"""Auto-discover all engines in the orion.engine package."""
|
|
30
|
+
if self._discovered:
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
# In-house proprietary commercial engines
|
|
34
|
+
engine_names = [
|
|
35
|
+
"sast", "secrets", "deps", "taint", "callgraph", "cfg", "symbol",
|
|
36
|
+
"dataflow", "authorization", "orphan", "prompt_security", "contract_probe",
|
|
37
|
+
"wiring_trace", "state", "license", "complexity", "duplication", "dead_import",
|
|
38
|
+
"error_handling", "logging_c", "config_engine", "chaos", "chaos_config",
|
|
39
|
+
"regression", "perf_guard", "data_residency", "noise", "supply_chain_sbom",
|
|
40
|
+
"hunter", "lint", "architecture", "test_coverage", "container",
|
|
41
|
+
"compliance", "provenance", "fp_learn", "sandbox_escape", "config_validation",
|
|
42
|
+
"iac_security", "crypto_guard", "llm_security", "api_security",
|
|
43
|
+
"threat_model", "sensitive_data"
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
for name in engine_names:
|
|
47
|
+
try:
|
|
48
|
+
mod = importlib.import_module(f"orion.engine.{name}.engine")
|
|
49
|
+
if hasattr(mod, "Engine"):
|
|
50
|
+
self._engines[name] = mod.Engine
|
|
51
|
+
except Exception as e:
|
|
52
|
+
# Engine not available or failed to load
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
self._discovered = True
|
|
Binary file
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Regression risk patterns detection engine - AST analysis for fragile patterns."""
|
|
2
|
+
import ast
|
|
3
|
+
from orion.engine.config import EngineConfig
|
|
4
|
+
from orion.engine.ir import OrionIR
|
|
5
|
+
from orion.engine.results import EngineResult, Finding
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Engine:
|
|
9
|
+
name = "regression"
|
|
10
|
+
version = "1.0.0"
|
|
11
|
+
|
|
12
|
+
async def check_availability(self) -> bool:
|
|
13
|
+
return True
|
|
14
|
+
|
|
15
|
+
def resource_limits(self):
|
|
16
|
+
from orion.engine.config import ResourceLimits
|
|
17
|
+
return ResourceLimits(max_cpu_seconds=60, max_memory_mb=256)
|
|
18
|
+
|
|
19
|
+
async def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
|
|
20
|
+
try:
|
|
21
|
+
text = ir.content.decode("utf-8", errors="replace")
|
|
22
|
+
except Exception:
|
|
23
|
+
return EngineResult(engine=self.name, findings=[], ok=True)
|
|
24
|
+
|
|
25
|
+
path = ir.metadata.get("path", "unknown")
|
|
26
|
+
is_test = self._is_test_file(path)
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
tree = ast.parse(text, filename=path)
|
|
30
|
+
except SyntaxError:
|
|
31
|
+
return EngineResult(engine=self.name, findings=[], ok=True)
|
|
32
|
+
|
|
33
|
+
findings = []
|
|
34
|
+
visitor = RegressionVisitor(path, is_test, text.splitlines())
|
|
35
|
+
visitor.visit(tree)
|
|
36
|
+
findings.extend(visitor.findings)
|
|
37
|
+
|
|
38
|
+
return EngineResult(engine=self.name, findings=findings, ok=True)
|
|
39
|
+
|
|
40
|
+
def _is_test_file(self, path: str) -> bool:
|
|
41
|
+
return any(p in path for p in ["test_", "_test.py", "/tests/", "/test/", "conftest.py"])
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class RegressionVisitor(ast.NodeVisitor):
|
|
45
|
+
def __init__(self, filepath: str, is_test: bool, lines: list[str]):
|
|
46
|
+
self.filepath = filepath
|
|
47
|
+
self.is_test = is_test
|
|
48
|
+
self.lines = lines
|
|
49
|
+
self.findings = []
|
|
50
|
+
self.module_level_code = True
|
|
51
|
+
|
|
52
|
+
def visit_Assign(self, node: ast.Assign):
|
|
53
|
+
if self.module_level_code:
|
|
54
|
+
for target in node.targets:
|
|
55
|
+
if isinstance(target, ast.Attribute):
|
|
56
|
+
if isinstance(target.value, ast.Name):
|
|
57
|
+
self._check_monkey_patching(target.value.id, target.attr, node)
|
|
58
|
+
elif isinstance(target, ast.Subscript):
|
|
59
|
+
# Check for sys.modules['x'] = y pattern
|
|
60
|
+
if (isinstance(target.value, ast.Attribute) and
|
|
61
|
+
isinstance(target.value.value, ast.Name) and
|
|
62
|
+
target.value.value.id == "sys" and
|
|
63
|
+
target.value.attr == "modules"):
|
|
64
|
+
self._add_finding(node, "sys_modules_mutation",
|
|
65
|
+
"sys.modules mutation detected",
|
|
66
|
+
"Modifying sys.modules at runtime causes import system instability",
|
|
67
|
+
confidence=0.9)
|
|
68
|
+
elif isinstance(target.value, ast.Name):
|
|
69
|
+
self._check_registry_mutation(target.value.id, node)
|
|
70
|
+
self.generic_visit(node)
|
|
71
|
+
|
|
72
|
+
def visit_Call(self, node: ast.Call):
|
|
73
|
+
func_name = self._get_call_name(node)
|
|
74
|
+
|
|
75
|
+
if func_name:
|
|
76
|
+
if func_name in ("setattr", "delattr"):
|
|
77
|
+
self._check_setattr_delattr(node, func_name)
|
|
78
|
+
elif func_name in ("__import__", "importlib.import_module", "importlib.util.module_from_spec"):
|
|
79
|
+
self._check_dynamic_import(node, func_name)
|
|
80
|
+
elif func_name == "signal.signal":
|
|
81
|
+
self._check_signal_handler(node)
|
|
82
|
+
elif func_name == "atexit.register":
|
|
83
|
+
self._check_atexit(node)
|
|
84
|
+
elif func_name in ("threading.Thread", "multiprocessing.Process", "concurrent.futures.ThreadPoolExecutor.submit"):
|
|
85
|
+
if self.module_level_code:
|
|
86
|
+
self._check_module_level_concurrency(node, func_name)
|
|
87
|
+
|
|
88
|
+
self.generic_visit(node)
|
|
89
|
+
|
|
90
|
+
def visit_ImportFrom(self, node: ast.ImportFrom):
|
|
91
|
+
if node.module == "builtins":
|
|
92
|
+
for alias in node.names:
|
|
93
|
+
if alias.name in ("__import__",):
|
|
94
|
+
pass
|
|
95
|
+
self.generic_visit(node)
|
|
96
|
+
|
|
97
|
+
def visit_FunctionDef(self, node: ast.FunctionDef):
|
|
98
|
+
old_module = self.module_level_code
|
|
99
|
+
self.module_level_code = False
|
|
100
|
+
|
|
101
|
+
if node.name in ("__getattr__", "__getattribute__", "__del__"):
|
|
102
|
+
self._add_finding(node, f"override_{node.name}",
|
|
103
|
+
f"Overriding {node.name}() - fragile and error-prone",
|
|
104
|
+
f"Consider alternatives to {node.name}() override",
|
|
105
|
+
confidence=0.85 if node.name == "__del__" else 0.8)
|
|
106
|
+
|
|
107
|
+
if self._is_os_environ_mutation(node):
|
|
108
|
+
self._check_os_environ_mutation(node)
|
|
109
|
+
|
|
110
|
+
self.generic_visit(node)
|
|
111
|
+
self.module_level_code = old_module
|
|
112
|
+
|
|
113
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
|
|
114
|
+
self.visit_FunctionDef(node)
|
|
115
|
+
|
|
116
|
+
def visit_ClassDef(self, node: ast.ClassDef):
|
|
117
|
+
self.generic_visit(node)
|
|
118
|
+
|
|
119
|
+
def _get_call_name(self, node: ast.Call) -> str | None:
|
|
120
|
+
if isinstance(node.func, ast.Name):
|
|
121
|
+
return node.func.id
|
|
122
|
+
elif isinstance(node.func, ast.Attribute):
|
|
123
|
+
parts = []
|
|
124
|
+
current = node.func
|
|
125
|
+
while isinstance(current, ast.Attribute):
|
|
126
|
+
parts.append(current.attr)
|
|
127
|
+
current = current.value
|
|
128
|
+
if isinstance(current, ast.Name):
|
|
129
|
+
parts.append(current.id)
|
|
130
|
+
return ".".join(reversed(parts))
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
def _check_monkey_patching(self, module_name: str, attr: str, node: ast.Assign):
|
|
134
|
+
if module_name in ("sys", "os", "builtins", "types", "importlib"):
|
|
135
|
+
self._add_finding(node, "monkey_patching_stdlib",
|
|
136
|
+
f"Monkey patching stdlib module {module_name}.{attr}",
|
|
137
|
+
"Avoid modifying standard library modules at runtime",
|
|
138
|
+
confidence=0.9)
|
|
139
|
+
|
|
140
|
+
def _check_registry_mutation(self, registry_name: str, node: ast.Assign):
|
|
141
|
+
registry_upper = registry_name.upper()
|
|
142
|
+
if "REGISTRY" in registry_upper or "REGISTRY" == registry_upper:
|
|
143
|
+
self._add_finding(node, "global_registry_mutation",
|
|
144
|
+
f"Mutating global registry {registry_name}",
|
|
145
|
+
"Global registry mutations cause test pollution and flaky tests",
|
|
146
|
+
confidence=0.85)
|
|
147
|
+
|
|
148
|
+
def _check_setattr_delattr(self, node: ast.Call, func_name: str):
|
|
149
|
+
if node.args and isinstance(node.args[0], ast.Name):
|
|
150
|
+
target = node.args[0].id
|
|
151
|
+
if target in ("sys", "os", "builtins", "types", "importlib", "logging"):
|
|
152
|
+
self._add_finding(node, f"{func_name}_stdlib",
|
|
153
|
+
f"{func_name}() on stdlib module {target}",
|
|
154
|
+
f"Avoid {func_name}() on standard library modules",
|
|
155
|
+
confidence=0.9)
|
|
156
|
+
|
|
157
|
+
def _check_dynamic_import(self, node: ast.Call, func_name: str):
|
|
158
|
+
# Only flag if importing directly with untrusted eval or non-literal module names
|
|
159
|
+
if node.args and not isinstance(node.args[0], ast.Constant):
|
|
160
|
+
self._add_finding(node, "dynamic_import_untrusted",
|
|
161
|
+
f"Dynamic import ({func_name}) with computed module name",
|
|
162
|
+
"Dynamic imports with non-constant module names can cause arbitrary module execution",
|
|
163
|
+
confidence=0.85)
|
|
164
|
+
|
|
165
|
+
def _check_signal_handler(self, node: ast.Call):
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
def _check_atexit(self, node: ast.Call):
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
def _check_module_level_concurrency(self, node: ast.Call, func_name: str):
|
|
172
|
+
self._add_finding(node, "module_level_concurrency",
|
|
173
|
+
f"Thread/process pool created at module level: {func_name}",
|
|
174
|
+
"Module-level concurrency initialization causes import-time side effects",
|
|
175
|
+
confidence=0.9)
|
|
176
|
+
|
|
177
|
+
def _is_os_environ_mutation(self, node: ast.FunctionDef) -> bool:
|
|
178
|
+
for stmt in ast.walk(node):
|
|
179
|
+
if isinstance(stmt, ast.Call):
|
|
180
|
+
func_name = self._get_call_name(stmt)
|
|
181
|
+
if func_name in ("os.environ.__setitem__", "os.environ.update", "os.putenv", "os.unsetenv"):
|
|
182
|
+
return True
|
|
183
|
+
if isinstance(stmt, ast.Assign):
|
|
184
|
+
for target in stmt.targets:
|
|
185
|
+
if isinstance(target, ast.Subscript):
|
|
186
|
+
if isinstance(target.value, ast.Attribute):
|
|
187
|
+
if isinstance(target.value.value, ast.Name) and target.value.value.id == "os":
|
|
188
|
+
if isinstance(target.value.attr, str) and target.value.attr == "environ":
|
|
189
|
+
return True
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
def _check_os_environ_mutation(self, node: ast.FunctionDef):
|
|
193
|
+
self._add_finding(node, "os_environ_mutation",
|
|
194
|
+
"os.environ mutation detected in function",
|
|
195
|
+
"Environment mutations affect global state and cause test pollution",
|
|
196
|
+
confidence=0.85)
|
|
197
|
+
|
|
198
|
+
def _add_finding(self, node: ast.AST, pattern: str, title: str, description: str, confidence: float):
|
|
199
|
+
line_no = getattr(node, "lineno", 0)
|
|
200
|
+
snippet = self.lines[line_no - 1].strip() if 0 < line_no <= len(self.lines) else ""
|
|
201
|
+
self.findings.append(Finding(
|
|
202
|
+
engine="regression",
|
|
203
|
+
severity="high" if confidence >= 0.85 else "medium",
|
|
204
|
+
title=title,
|
|
205
|
+
description=description,
|
|
206
|
+
location=f"{self.filepath}:{line_no}",
|
|
207
|
+
metadata={
|
|
208
|
+
"confidence": confidence,
|
|
209
|
+
"pattern": pattern,
|
|
210
|
+
"snippet": snippet[:200],
|
|
211
|
+
"cwe": "CWE-474" if "monkey" in pattern or "dynamic_import" in pattern else "CWE-667",
|
|
212
|
+
},
|
|
213
|
+
))
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class Finding:
|
|
7
|
+
engine: str
|
|
8
|
+
severity: str
|
|
9
|
+
title: str
|
|
10
|
+
description: str = ""
|
|
11
|
+
location: str = ""
|
|
12
|
+
metadata: dict = field(default_factory=dict)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class EngineResult:
|
|
17
|
+
engine: str
|
|
18
|
+
findings: list = field(default_factory=list)
|
|
19
|
+
errors: list = field(default_factory=list)
|
|
20
|
+
metrics: dict = field(default_factory=dict)
|
|
21
|
+
ok: bool = True
|