@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,268 +0,0 @@
|
|
|
1
|
-
"""Chaos injection analysis engine - AST mutation analysis for resilience 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 = "chaos"
|
|
10
|
-
version = "1.0.0"
|
|
11
|
-
|
|
12
|
-
EXTERNAL_CALL_PATTERNS = {
|
|
13
|
-
"requests.get", "requests.post", "requests.put", "requests.delete", "requests.request",
|
|
14
|
-
"httpx.get", "httpx.post", "httpx.put", "httpx.delete", "httpx.request",
|
|
15
|
-
"aiohttp.ClientSession.get", "aiohttp.ClientSession.post",
|
|
16
|
-
"urllib.request.urlopen", "urllib3.request",
|
|
17
|
-
"boto3.client", "boto3.resource",
|
|
18
|
-
"redis.Redis", "redis.StrictRedis",
|
|
19
|
-
"psycopg2.connect", "mysql.connector.connect", "pymongo.MongoClient",
|
|
20
|
-
"sqlalchemy.create_engine", "asyncpg.connect",
|
|
21
|
-
"kafka.KafkaProducer", "kafka.KafkaConsumer",
|
|
22
|
-
"pika.BlockingConnection", "pika.SelectConnection",
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
RETRY_PATTERNS = {"retry", "backoff", "tenacity", "retrying"}
|
|
26
|
-
CIRCUIT_BREAKER_PATTERNS = {"circuitbreaker", "circuit_breaker", "pybreaker", "hystrix"}
|
|
27
|
-
TIMEOUT_PATTERNS = {"timeout", "connect_timeout", "read_timeout", "request_timeout"}
|
|
28
|
-
|
|
29
|
-
async def check_availability(self) -> bool:
|
|
30
|
-
return True
|
|
31
|
-
|
|
32
|
-
def resource_limits(self):
|
|
33
|
-
from orion.engine.config import ResourceLimits
|
|
34
|
-
return ResourceLimits(max_cpu_seconds=120, max_memory_mb=512)
|
|
35
|
-
|
|
36
|
-
async def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
|
|
37
|
-
try:
|
|
38
|
-
text = ir.content.decode("utf-8", errors="replace")
|
|
39
|
-
except Exception:
|
|
40
|
-
return EngineResult(engine=self.name, findings=[], ok=True)
|
|
41
|
-
|
|
42
|
-
path = ir.metadata.get("path", "unknown")
|
|
43
|
-
is_test = self._is_test_file(path)
|
|
44
|
-
|
|
45
|
-
try:
|
|
46
|
-
tree = ast.parse(text, filename=path)
|
|
47
|
-
except SyntaxError:
|
|
48
|
-
return EngineResult(engine=self.name, findings=[], ok=True)
|
|
49
|
-
|
|
50
|
-
findings = []
|
|
51
|
-
visitor = ChaosVisitor(path, is_test, text.splitlines())
|
|
52
|
-
visitor.visit(tree)
|
|
53
|
-
findings.extend(visitor.findings)
|
|
54
|
-
|
|
55
|
-
return EngineResult(engine=self.name, findings=findings, ok=True)
|
|
56
|
-
|
|
57
|
-
def _is_test_file(self, path: str) -> bool:
|
|
58
|
-
return any(p in path for p in ["test_", "_test.py", "/tests/", "/test/", "conftest.py"])
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
class ChaosVisitor(ast.NodeVisitor):
|
|
62
|
-
EXTERNAL_CALL_PATTERNS = {
|
|
63
|
-
"requests.get", "requests.post", "requests.put", "requests.delete", "requests.request",
|
|
64
|
-
"httpx.get", "httpx.post", "httpx.put", "httpx.delete", "httpx.request",
|
|
65
|
-
"aiohttp.ClientSession.get", "aiohttp.ClientSession.post",
|
|
66
|
-
"urllib.request.urlopen", "urllib3.request",
|
|
67
|
-
"boto3.client", "boto3.resource",
|
|
68
|
-
"redis.Redis", "redis.StrictRedis",
|
|
69
|
-
"psycopg2.connect", "mysql.connector.connect", "pymongo.MongoClient",
|
|
70
|
-
"sqlalchemy.create_engine", "asyncpg.connect",
|
|
71
|
-
"kafka.KafkaProducer", "kafka.KafkaConsumer",
|
|
72
|
-
"pika.BlockingConnection", "pika.SelectConnection",
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
RETRY_PATTERNS = {"retry", "backoff", "tenacity", "retrying"}
|
|
76
|
-
CIRCUIT_BREAKER_PATTERNS = {"circuitbreaker", "circuit_breaker", "pybreaker", "hystrix"}
|
|
77
|
-
TIMEOUT_PATTERNS = {"timeout", "connect_timeout", "read_timeout", "request_timeout"}
|
|
78
|
-
|
|
79
|
-
def __init__(self, filepath: str, is_test: bool, lines: list[str]):
|
|
80
|
-
self.filepath = filepath
|
|
81
|
-
self.is_test = is_test
|
|
82
|
-
self.lines = lines
|
|
83
|
-
self.findings = []
|
|
84
|
-
self.external_calls = []
|
|
85
|
-
self.has_retry = False
|
|
86
|
-
self.has_circuit_breaker = False
|
|
87
|
-
self.has_timeout = False
|
|
88
|
-
self.has_idempotency = False
|
|
89
|
-
self.global_state = set()
|
|
90
|
-
self.global_locks = set()
|
|
91
|
-
self.direct_instantiations = []
|
|
92
|
-
self.health_check_endpoints = []
|
|
93
|
-
self.module_level_threads = []
|
|
94
|
-
self.in_function = False
|
|
95
|
-
self.current_function = None
|
|
96
|
-
|
|
97
|
-
def visit_FunctionDef(self, node: ast.FunctionDef):
|
|
98
|
-
old_func = self.current_function
|
|
99
|
-
self.current_function = node.name
|
|
100
|
-
old_in_func = self.in_function
|
|
101
|
-
self.in_function = True
|
|
102
|
-
|
|
103
|
-
# First visit body to collect external calls
|
|
104
|
-
for stmt in node.body:
|
|
105
|
-
self.visit(stmt)
|
|
106
|
-
|
|
107
|
-
# Then check for chaos patterns
|
|
108
|
-
self._check_function_for_chaos_patterns(node)
|
|
109
|
-
|
|
110
|
-
self.in_function = old_in_func
|
|
111
|
-
self.current_function = old_func
|
|
112
|
-
|
|
113
|
-
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
|
|
114
|
-
self.visit_FunctionDef(node)
|
|
115
|
-
|
|
116
|
-
def visit_Call(self, node: ast.Call):
|
|
117
|
-
self._analyze_call(node)
|
|
118
|
-
self.generic_visit(node)
|
|
119
|
-
|
|
120
|
-
def visit_Assign(self, node: ast.Assign):
|
|
121
|
-
if not self.in_function:
|
|
122
|
-
for target in node.targets:
|
|
123
|
-
if isinstance(target, ast.Name):
|
|
124
|
-
self.global_state.add(target.id)
|
|
125
|
-
if self._is_lock_creation(node.value):
|
|
126
|
-
self.global_locks.add(target.id)
|
|
127
|
-
self.generic_visit(node)
|
|
128
|
-
|
|
129
|
-
def visit_ClassDef(self, node: ast.ClassDef):
|
|
130
|
-
self.generic_visit(node)
|
|
131
|
-
|
|
132
|
-
def _analyze_call(self, node: ast.Call):
|
|
133
|
-
func_name = self._get_call_name(node)
|
|
134
|
-
if not func_name:
|
|
135
|
-
return
|
|
136
|
-
|
|
137
|
-
if any(pattern in func_name for pattern in self.EXTERNAL_CALL_PATTERNS):
|
|
138
|
-
self.external_calls.append((node, func_name))
|
|
139
|
-
|
|
140
|
-
if any(pattern in func_name.lower() for pattern in self.RETRY_PATTERNS):
|
|
141
|
-
self.has_retry = True
|
|
142
|
-
|
|
143
|
-
if any(pattern in func_name.lower() for pattern in self.CIRCUIT_BREAKER_PATTERNS):
|
|
144
|
-
self.has_circuit_breaker = True
|
|
145
|
-
|
|
146
|
-
if "timeout" in func_name.lower():
|
|
147
|
-
self.has_timeout = True
|
|
148
|
-
|
|
149
|
-
for kw in node.keywords:
|
|
150
|
-
if kw.arg in ("timeout", "connect_timeout", "read_timeout", "request_timeout"):
|
|
151
|
-
self.has_timeout = True
|
|
152
|
-
if kw.arg in ("idempotency_key", "idempotent"):
|
|
153
|
-
self.has_idempotency = True
|
|
154
|
-
|
|
155
|
-
if self._is_direct_instantiation(node):
|
|
156
|
-
self.direct_instantiations.append((node, func_name))
|
|
157
|
-
|
|
158
|
-
def _get_call_name(self, node: ast.Call) -> str | None:
|
|
159
|
-
if isinstance(node.func, ast.Name):
|
|
160
|
-
return node.func.id
|
|
161
|
-
elif isinstance(node.func, ast.Attribute):
|
|
162
|
-
parts = []
|
|
163
|
-
current = node.func
|
|
164
|
-
while isinstance(current, ast.Attribute):
|
|
165
|
-
parts.append(current.attr)
|
|
166
|
-
current = current.value
|
|
167
|
-
if isinstance(current, ast.Name):
|
|
168
|
-
parts.append(current.id)
|
|
169
|
-
return ".".join(reversed(parts))
|
|
170
|
-
return None
|
|
171
|
-
|
|
172
|
-
def _is_lock_creation(self, node: ast.AST) -> bool:
|
|
173
|
-
if isinstance(node, ast.Call):
|
|
174
|
-
name = self._get_call_name(node)
|
|
175
|
-
return name and ("Lock" in name or "RLock" in name or "Semaphore" in name)
|
|
176
|
-
return False
|
|
177
|
-
|
|
178
|
-
def _is_direct_instantiation(self, node: ast.Call) -> bool:
|
|
179
|
-
if isinstance(node.func, ast.Name):
|
|
180
|
-
return node.func.id[0].isupper() if node.func.id else False
|
|
181
|
-
elif isinstance(node.func, ast.Attribute):
|
|
182
|
-
return node.func.attr[0].isupper() if node.func.attr else False
|
|
183
|
-
return False
|
|
184
|
-
|
|
185
|
-
def _check_function_for_chaos_patterns(self, node: ast.FunctionDef):
|
|
186
|
-
if self.external_calls and not self.has_retry:
|
|
187
|
-
for call_node, call_name in self.external_calls:
|
|
188
|
-
self._add_finding(call_node, "no_retry",
|
|
189
|
-
f"External call '{call_name}' without retry logic",
|
|
190
|
-
f"Add retry with exponential backoff for resilience",
|
|
191
|
-
confidence=0.8)
|
|
192
|
-
|
|
193
|
-
if self.external_calls and not self.has_circuit_breaker:
|
|
194
|
-
for call_node, call_name in self.external_calls:
|
|
195
|
-
self._add_finding(call_node, "no_circuit_breaker",
|
|
196
|
-
f"External call '{call_name}' without circuit breaker",
|
|
197
|
-
f"Add circuit breaker pattern to prevent cascade failures",
|
|
198
|
-
confidence=0.75)
|
|
199
|
-
|
|
200
|
-
if self.external_calls and not self.has_timeout:
|
|
201
|
-
for call_node, call_name in self.external_calls:
|
|
202
|
-
self._add_finding(call_node, "no_timeout",
|
|
203
|
-
f"External call '{call_name}' without timeout",
|
|
204
|
-
f"Add timeout parameter (e.g., timeout=5)",
|
|
205
|
-
confidence=0.85)
|
|
206
|
-
|
|
207
|
-
if self.external_calls and not self.has_idempotency:
|
|
208
|
-
for call_node, call_name in self.external_calls:
|
|
209
|
-
if "post" in call_name.lower() or "put" in call_name.lower() or "patch" in call_name.lower():
|
|
210
|
-
self._add_finding(call_node, "no_idempotency_key",
|
|
211
|
-
f"Mutating external call '{call_name}' without idempotency key",
|
|
212
|
-
f"Add idempotency_key parameter for safe retries",
|
|
213
|
-
confidence=0.7)
|
|
214
|
-
|
|
215
|
-
def visit_Module(self, node: ast.Module):
|
|
216
|
-
for stmt in node.body:
|
|
217
|
-
if isinstance(stmt, (ast.Import, ast.ImportFrom)):
|
|
218
|
-
for alias in stmt.names:
|
|
219
|
-
if alias.name in ("threading", "multiprocessing", "concurrent.futures"):
|
|
220
|
-
pass
|
|
221
|
-
elif isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
222
|
-
pass
|
|
223
|
-
elif isinstance(stmt, ast.Assign):
|
|
224
|
-
self.visit(stmt)
|
|
225
|
-
elif isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call):
|
|
226
|
-
call_name = self._get_call_name(stmt.value)
|
|
227
|
-
if call_name and "Thread" in call_name:
|
|
228
|
-
self.module_level_threads.append((stmt, call_name))
|
|
229
|
-
|
|
230
|
-
self._check_module_level_patterns()
|
|
231
|
-
self.generic_visit(node)
|
|
232
|
-
|
|
233
|
-
def _check_module_level_patterns(self):
|
|
234
|
-
for thread_node, thread_name in self.module_level_threads:
|
|
235
|
-
self._add_finding(thread_node, "module_level_threading",
|
|
236
|
-
f"Thread/process started at module level: {thread_name}",
|
|
237
|
-
"Move thread initialization to function or use lazy initialization",
|
|
238
|
-
confidence=0.8)
|
|
239
|
-
|
|
240
|
-
if len(self.global_locks) > 0:
|
|
241
|
-
for lock in self.global_locks:
|
|
242
|
-
self._add_finding(None, "global_lock_bottleneck",
|
|
243
|
-
f"Global lock '{lock}' may create single-threaded bottleneck",
|
|
244
|
-
"Consider fine-grained locking or lock-free data structures",
|
|
245
|
-
confidence=0.75, line_no=1)
|
|
246
|
-
|
|
247
|
-
def _add_finding(self, node: ast.AST | None, pattern: str, title: str, fix: str, confidence: float, line_no: int | None = None):
|
|
248
|
-
if node:
|
|
249
|
-
line_no = getattr(node, "lineno", 1)
|
|
250
|
-
elif line_no is None:
|
|
251
|
-
line_no = 1
|
|
252
|
-
|
|
253
|
-
snippet = self.lines[line_no - 1].strip() if 0 < line_no <= len(self.lines) else ""
|
|
254
|
-
|
|
255
|
-
self.findings.append(Finding(
|
|
256
|
-
engine="chaos",
|
|
257
|
-
severity="high" if confidence >= 0.8 else "medium",
|
|
258
|
-
title=title,
|
|
259
|
-
description=fix,
|
|
260
|
-
location=f"{self.filepath}:{line_no}",
|
|
261
|
-
metadata={
|
|
262
|
-
"confidence": confidence,
|
|
263
|
-
"pattern": pattern,
|
|
264
|
-
"location": f"{self.filepath}:{line_no}",
|
|
265
|
-
"fix": fix,
|
|
266
|
-
"snippet": snippet[:200],
|
|
267
|
-
},
|
|
268
|
-
))
|
|
Binary file
|
|
@@ -1,322 +0,0 @@
|
|
|
1
|
-
"""Config chaos analysis engine - edge case testing for configuration files."""
|
|
2
|
-
import json
|
|
3
|
-
import re
|
|
4
|
-
import tomllib
|
|
5
|
-
from pathlib import Path
|
|
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
|
-
class Engine:
|
|
12
|
-
name = "chaos_config"
|
|
13
|
-
version = "1.0.0"
|
|
14
|
-
|
|
15
|
-
POSITIVE_ONLY_KEYS = {
|
|
16
|
-
"port", "PORT", "timeout", "TIMEOUT", "retries", "RETRIES", "max_retries",
|
|
17
|
-
"workers", "WORKERS", "pool_size", "POOL_SIZE", "max_connections",
|
|
18
|
-
"MAX_CONNECTIONS", "buffer_size", "BUFFER_SIZE", "batch_size", "BATCH_SIZE",
|
|
19
|
-
"limit", "LIMIT", "page_size", "PAGE_SIZE", "ttl", "TTL", "expire", "EXPIRE",
|
|
20
|
-
"interval", "INTERVAL", "rate_limit", "RATE_LIMIT", "max_size", "MAX_SIZE",
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
ENUM_KEYS = {
|
|
24
|
-
"log_level": {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"},
|
|
25
|
-
"LOG_LEVEL": {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"},
|
|
26
|
-
"environment": {"development", "staging", "production", "test"},
|
|
27
|
-
"ENVIRONMENT": {"development", "staging", "production", "test"},
|
|
28
|
-
"mode": {"sync", "async", "threaded", "multiprocess"},
|
|
29
|
-
"protocol": {"http", "https", "grpc", "tcp", "udp"},
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
REQUIRED_SECTIONS = {
|
|
33
|
-
"database", "redis", "kafka", "rabbitmq", "email", "storage", "auth",
|
|
34
|
-
"logging", "monitoring", "security", "cache", "queue",
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async def check_availability(self) -> bool:
|
|
38
|
-
return True
|
|
39
|
-
|
|
40
|
-
def resource_limits(self):
|
|
41
|
-
from orion.engine.config import ResourceLimits
|
|
42
|
-
return ResourceLimits(max_cpu_seconds=60, max_memory_mb=256)
|
|
43
|
-
|
|
44
|
-
async def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
|
|
45
|
-
try:
|
|
46
|
-
text = ir.content.decode("utf-8", errors="replace")
|
|
47
|
-
except Exception:
|
|
48
|
-
return EngineResult(engine=self.name, findings=[], ok=True)
|
|
49
|
-
|
|
50
|
-
path = ir.metadata.get("path", "unknown")
|
|
51
|
-
filename = Path(path).name.lower()
|
|
52
|
-
ext = Path(path).suffix.lower()
|
|
53
|
-
|
|
54
|
-
findings = []
|
|
55
|
-
parsed = self._parse_config(text, filename, ext)
|
|
56
|
-
|
|
57
|
-
if parsed is not None:
|
|
58
|
-
flat = self._flatten_config(parsed)
|
|
59
|
-
findings.extend(self._check_edge_cases(flat, path, filename, ext))
|
|
60
|
-
findings.extend(self._check_circular_refs(flat, path))
|
|
61
|
-
findings.extend(self._check_overly_long(flat, path))
|
|
62
|
-
findings.extend(self._check_special_chars(flat, path))
|
|
63
|
-
|
|
64
|
-
return EngineResult(engine=self.name, findings=findings, ok=True)
|
|
65
|
-
|
|
66
|
-
def _parse_config(self, text: str, filename: str, ext: str) -> dict | None:
|
|
67
|
-
try:
|
|
68
|
-
if ext in (".yaml", ".yml"):
|
|
69
|
-
import yaml
|
|
70
|
-
return yaml.safe_load(text) or {}
|
|
71
|
-
elif ext == ".json":
|
|
72
|
-
return json.loads(text) or {}
|
|
73
|
-
elif ext == ".toml":
|
|
74
|
-
return tomllib.loads(text) or {}
|
|
75
|
-
elif ext in (".env",):
|
|
76
|
-
return self._parse_env(text)
|
|
77
|
-
elif ext in (".ini", ".cfg", ".conf"):
|
|
78
|
-
return self._parse_ini(text)
|
|
79
|
-
elif ext == ".properties":
|
|
80
|
-
return self._parse_properties(text)
|
|
81
|
-
except Exception:
|
|
82
|
-
pass
|
|
83
|
-
return None
|
|
84
|
-
|
|
85
|
-
def _parse_env(self, text: str) -> dict:
|
|
86
|
-
result = {}
|
|
87
|
-
for line in text.splitlines():
|
|
88
|
-
line = line.strip()
|
|
89
|
-
if not line or line.startswith("#"):
|
|
90
|
-
continue
|
|
91
|
-
if "=" in line:
|
|
92
|
-
key, value = line.split("=", 1)
|
|
93
|
-
result[key.strip()] = value.strip().strip('"\'')
|
|
94
|
-
return result
|
|
95
|
-
|
|
96
|
-
def _parse_ini(self, text: str) -> dict:
|
|
97
|
-
import configparser
|
|
98
|
-
parser = configparser.ConfigParser()
|
|
99
|
-
parser.read_string(text)
|
|
100
|
-
result = {}
|
|
101
|
-
for section in parser.sections():
|
|
102
|
-
for key, value in parser[section].items():
|
|
103
|
-
result[f"{section}.{key}"] = value
|
|
104
|
-
return result
|
|
105
|
-
|
|
106
|
-
def _parse_properties(self, text: str) -> dict:
|
|
107
|
-
result = {}
|
|
108
|
-
for line in text.splitlines():
|
|
109
|
-
line = line.strip()
|
|
110
|
-
if not line or line.startswith("#") or line.startswith("!"):
|
|
111
|
-
continue
|
|
112
|
-
if "=" in line:
|
|
113
|
-
key, value = line.split("=", 1)
|
|
114
|
-
result[key.strip()] = value.strip()
|
|
115
|
-
elif ":" in line:
|
|
116
|
-
key, value = line.split(":", 1)
|
|
117
|
-
result[key.strip()] = value.strip()
|
|
118
|
-
return result
|
|
119
|
-
|
|
120
|
-
def _flatten_config(self, config: dict, prefix: str = "") -> dict:
|
|
121
|
-
result = {}
|
|
122
|
-
for key, value in config.items():
|
|
123
|
-
full_key = f"{prefix}.{key}" if prefix else key
|
|
124
|
-
if isinstance(value, dict):
|
|
125
|
-
result.update(self._flatten_config(value, full_key))
|
|
126
|
-
else:
|
|
127
|
-
result[full_key] = value
|
|
128
|
-
return result
|
|
129
|
-
|
|
130
|
-
def _check_edge_cases(self, flat: dict, path: str, filename: str, ext: str) -> list[Finding]:
|
|
131
|
-
findings = []
|
|
132
|
-
for key, value in flat.items():
|
|
133
|
-
key_lower = key.lower()
|
|
134
|
-
|
|
135
|
-
if value is None or (isinstance(value, str) and value.strip() == ""):
|
|
136
|
-
findings.append(Finding(
|
|
137
|
-
engine=self.name,
|
|
138
|
-
severity="high",
|
|
139
|
-
title=f"Empty/null config value: {key}",
|
|
140
|
-
description=f"Config key '{key}' has empty or null value which may cause failures",
|
|
141
|
-
location=path,
|
|
142
|
-
metadata={
|
|
143
|
-
"confidence": 0.9,
|
|
144
|
-
"key": key,
|
|
145
|
-
"value": "null" if value is None else '""',
|
|
146
|
-
"risk": "null_or_empty_value",
|
|
147
|
-
},
|
|
148
|
-
))
|
|
149
|
-
|
|
150
|
-
if isinstance(value, (int, float)):
|
|
151
|
-
if key_lower in self.POSITIVE_ONLY_KEYS and value <= 0:
|
|
152
|
-
findings.append(Finding(
|
|
153
|
-
engine=self.name,
|
|
154
|
-
severity="high",
|
|
155
|
-
title=f"Negative/zero value for positive-only key: {key}",
|
|
156
|
-
description=f"Key '{key}' = {value} but must be positive",
|
|
157
|
-
location=path,
|
|
158
|
-
metadata={
|
|
159
|
-
"confidence": 0.95,
|
|
160
|
-
"key": key,
|
|
161
|
-
"value": str(value),
|
|
162
|
-
"risk": "negative_where_positive_expected",
|
|
163
|
-
},
|
|
164
|
-
))
|
|
165
|
-
if isinstance(value, (int, float)) and abs(value) > 1e9:
|
|
166
|
-
findings.append(Finding(
|
|
167
|
-
engine=self.name,
|
|
168
|
-
severity="medium",
|
|
169
|
-
title=f"Extremely large numeric value: {key}",
|
|
170
|
-
description=f"Key '{key}' = {value} may cause overflow or performance issues",
|
|
171
|
-
location=path,
|
|
172
|
-
metadata={
|
|
173
|
-
"confidence": 0.75,
|
|
174
|
-
"key": key,
|
|
175
|
-
"value": str(value),
|
|
176
|
-
"risk": "very_large_number",
|
|
177
|
-
},
|
|
178
|
-
))
|
|
179
|
-
|
|
180
|
-
if isinstance(value, str):
|
|
181
|
-
if key_lower in self.ENUM_KEYS:
|
|
182
|
-
allowed = self.ENUM_KEYS[key_lower]
|
|
183
|
-
if value.upper() not in {v.upper() for v in allowed}:
|
|
184
|
-
findings.append(Finding(
|
|
185
|
-
engine=self.name,
|
|
186
|
-
severity="high",
|
|
187
|
-
title=f"Invalid enum value: {key}",
|
|
188
|
-
description=f"Key '{key}' = '{value}' not in allowed values: {allowed}",
|
|
189
|
-
location=path,
|
|
190
|
-
metadata={
|
|
191
|
-
"confidence": 0.95,
|
|
192
|
-
"key": key,
|
|
193
|
-
"value": value,
|
|
194
|
-
"risk": "invalid_enum",
|
|
195
|
-
"allowed": list(allowed),
|
|
196
|
-
},
|
|
197
|
-
))
|
|
198
|
-
|
|
199
|
-
return findings
|
|
200
|
-
|
|
201
|
-
def _check_circular_refs(self, flat: dict, path: str) -> list[Finding]:
|
|
202
|
-
findings = []
|
|
203
|
-
visited = set()
|
|
204
|
-
path_stack = []
|
|
205
|
-
|
|
206
|
-
def check_refs(obj, current_path=""):
|
|
207
|
-
if id(obj) in visited:
|
|
208
|
-
return
|
|
209
|
-
visited.add(id(obj))
|
|
210
|
-
|
|
211
|
-
if isinstance(obj, dict):
|
|
212
|
-
for k, v in obj.items():
|
|
213
|
-
new_path = f"{current_path}.{k}" if current_path else k
|
|
214
|
-
if isinstance(v, str) and v.startswith("${") and v.endswith("}"):
|
|
215
|
-
ref_key = v[2:-1]
|
|
216
|
-
if ref_key in path_stack:
|
|
217
|
-
findings.append(Finding(
|
|
218
|
-
engine=self.name,
|
|
219
|
-
severity="high",
|
|
220
|
-
title=f"Circular reference detected: {new_path}",
|
|
221
|
-
description=f"Config reference '{v}' creates circular dependency",
|
|
222
|
-
location=path,
|
|
223
|
-
metadata={
|
|
224
|
-
"confidence": 0.9,
|
|
225
|
-
"key": new_path,
|
|
226
|
-
"value": v,
|
|
227
|
-
"risk": "circular_reference",
|
|
228
|
-
"cycle": " -> ".join(path_stack + [ref_key]),
|
|
229
|
-
},
|
|
230
|
-
))
|
|
231
|
-
path_stack.append(k)
|
|
232
|
-
check_refs(v, new_path)
|
|
233
|
-
path_stack.pop()
|
|
234
|
-
elif isinstance(obj, list):
|
|
235
|
-
for i, item in enumerate(obj):
|
|
236
|
-
check_refs(item, f"{current_path}[{i}]")
|
|
237
|
-
|
|
238
|
-
check_refs(flat)
|
|
239
|
-
return findings
|
|
240
|
-
|
|
241
|
-
def _check_overly_long(self, flat: dict, path: str) -> list[Finding]:
|
|
242
|
-
findings = []
|
|
243
|
-
for key, value in flat.items():
|
|
244
|
-
if isinstance(value, str) and len(value) > 10000:
|
|
245
|
-
findings.append(Finding(
|
|
246
|
-
engine=self.name,
|
|
247
|
-
severity="medium",
|
|
248
|
-
title=f"Overly long config value: {key}",
|
|
249
|
-
description=f"Config value for '{key}' exceeds 10000 characters ({len(value)} chars)",
|
|
250
|
-
location=path,
|
|
251
|
-
metadata={
|
|
252
|
-
"confidence": 0.8,
|
|
253
|
-
"key": key,
|
|
254
|
-
"value_length": len(value),
|
|
255
|
-
"risk": "overly_long_string",
|
|
256
|
-
},
|
|
257
|
-
))
|
|
258
|
-
return findings
|
|
259
|
-
|
|
260
|
-
def _check_special_chars(self, flat: dict, path: str) -> list[Finding]:
|
|
261
|
-
findings = []
|
|
262
|
-
dangerous_chars = ["\0", "\r", "\x1b", "\x00", "\x01", "\x02", "\x03", "\x04"]
|
|
263
|
-
for key, value in flat.items():
|
|
264
|
-
if isinstance(value, str):
|
|
265
|
-
for char in dangerous_chars:
|
|
266
|
-
if char in value:
|
|
267
|
-
findings.append(Finding(
|
|
268
|
-
engine=self.name,
|
|
269
|
-
severity="high",
|
|
270
|
-
title=f"Dangerous character in config: {key}",
|
|
271
|
-
description=f"Config value contains control character (0x{ord(char):02x})",
|
|
272
|
-
location=path,
|
|
273
|
-
metadata={
|
|
274
|
-
"confidence": 0.9,
|
|
275
|
-
"key": key,
|
|
276
|
-
"risk": "control_character",
|
|
277
|
-
"char_code": f"0x{ord(char):02x}",
|
|
278
|
-
},
|
|
279
|
-
))
|
|
280
|
-
break
|
|
281
|
-
|
|
282
|
-
if key.lower().endswith(("path", "dir", "directory", "file", "cert", "key")):
|
|
283
|
-
if any(c in value for c in ["|", "&", ";", "$", "`", ">", "<", "\n", "\r"]):
|
|
284
|
-
findings.append(Finding(
|
|
285
|
-
engine=self.name,
|
|
286
|
-
severity="high",
|
|
287
|
-
title=f"Shell metacharacters in path config: {key}",
|
|
288
|
-
description=f"Path value contains shell metacharacters - potential injection",
|
|
289
|
-
location=path,
|
|
290
|
-
metadata={
|
|
291
|
-
"confidence": 0.85,
|
|
292
|
-
"key": key,
|
|
293
|
-
"risk": "shell_metacharacters_in_path",
|
|
294
|
-
},
|
|
295
|
-
))
|
|
296
|
-
return findings
|
|
297
|
-
|
|
298
|
-
def _check_missing_sections(self, parsed: dict, path: str, filename: str) -> list[Finding]:
|
|
299
|
-
findings = []
|
|
300
|
-
if not parsed:
|
|
301
|
-
return findings
|
|
302
|
-
|
|
303
|
-
top_keys = {k.lower() for k in parsed.keys() if not isinstance(parsed[k], dict)}
|
|
304
|
-
section_keys = {k.lower() for k in parsed.keys() if isinstance(parsed[k], dict)}
|
|
305
|
-
|
|
306
|
-
for req in self.REQUIRED_SECTIONS:
|
|
307
|
-
if req not in section_keys and req not in top_keys:
|
|
308
|
-
if any(req in k for k in section_keys) or any(req in k for k in top_keys):
|
|
309
|
-
continue
|
|
310
|
-
findings.append(Finding(
|
|
311
|
-
engine=self.name,
|
|
312
|
-
severity="medium",
|
|
313
|
-
title=f"Missing required config section: {req}",
|
|
314
|
-
description=f"Expected configuration section '{req}' not found",
|
|
315
|
-
location=path,
|
|
316
|
-
metadata={
|
|
317
|
-
"confidence": 0.7,
|
|
318
|
-
"key": req,
|
|
319
|
-
"issue": "missing_section",
|
|
320
|
-
},
|
|
321
|
-
))
|
|
322
|
-
return findings
|
|
Binary file
|