@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.
Files changed (129) hide show
  1. package/README.md +80 -190
  2. package/bin/orion.js +30 -29
  3. package/dist/orion.exe +0 -0
  4. package/package.json +46 -48
  5. package/docs/API_REFERENCE.md +0 -211
  6. package/docs/ARCHITECTURE.md +0 -183
  7. package/docs/CHANGELOG.md +0 -112
  8. package/docs/CONTRIBUTING.md +0 -157
  9. package/docs/ENGINE_CATALOG.md +0 -87
  10. package/docs/GETTING_STARTED.md +0 -226
  11. package/pyproject.toml +0 -16
  12. package/src/orion/__init__.py +0 -0
  13. package/src/orion/__pycache__/__init__.cpython-313.pyc +0 -0
  14. package/src/orion/engine/__init__.py +0 -17
  15. package/src/orion/engine/__pycache__/__init__.cpython-313.pyc +0 -0
  16. package/src/orion/engine/__pycache__/config.cpython-313.pyc +0 -0
  17. package/src/orion/engine/__pycache__/core.cpython-313.pyc +0 -0
  18. package/src/orion/engine/__pycache__/ir.cpython-313.pyc +0 -0
  19. package/src/orion/engine/__pycache__/languages.cpython-313.pyc +0 -0
  20. package/src/orion/engine/__pycache__/registry.cpython-313.pyc +0 -0
  21. package/src/orion/engine/__pycache__/results.cpython-313.pyc +0 -0
  22. package/src/orion/engine/__pycache__/runner.cpython-313.pyc +0 -0
  23. package/src/orion/engine/api_security/__pycache__/engine.cpython-313.pyc +0 -0
  24. package/src/orion/engine/api_security/engine.py +0 -125
  25. package/src/orion/engine/architecture/__pycache__/engine.cpython-313.pyc +0 -0
  26. package/src/orion/engine/architecture/engine.py +0 -463
  27. package/src/orion/engine/authorization/__pycache__/engine.cpython-313.pyc +0 -0
  28. package/src/orion/engine/authorization/engine.py +0 -337
  29. package/src/orion/engine/callgraph/__pycache__/engine.cpython-313.pyc +0 -0
  30. package/src/orion/engine/callgraph/engine.py +0 -236
  31. package/src/orion/engine/cfg/__pycache__/engine.cpython-313.pyc +0 -0
  32. package/src/orion/engine/cfg/engine.py +0 -509
  33. package/src/orion/engine/chaos/__pycache__/engine.cpython-313.pyc +0 -0
  34. package/src/orion/engine/chaos/engine.py +0 -268
  35. package/src/orion/engine/chaos_config/__pycache__/engine.cpython-313.pyc +0 -0
  36. package/src/orion/engine/chaos_config/engine.py +0 -322
  37. package/src/orion/engine/complexity/__pycache__/engine.cpython-313.pyc +0 -0
  38. package/src/orion/engine/complexity/engine.py +0 -144
  39. package/src/orion/engine/compliance/__pycache__/engine.cpython-313.pyc +0 -0
  40. package/src/orion/engine/compliance/engine.py +0 -325
  41. package/src/orion/engine/config.py +0 -41
  42. package/src/orion/engine/config_engine/__pycache__/engine.cpython-313.pyc +0 -0
  43. package/src/orion/engine/config_engine/engine.py +0 -364
  44. package/src/orion/engine/config_validation/__pycache__/engine.cpython-313.pyc +0 -0
  45. package/src/orion/engine/config_validation/engine.py +0 -313
  46. package/src/orion/engine/container/__pycache__/engine.cpython-313.pyc +0 -0
  47. package/src/orion/engine/container/engine.py +0 -546
  48. package/src/orion/engine/contract_probe/__pycache__/engine.cpython-313.pyc +0 -0
  49. package/src/orion/engine/contract_probe/engine.py +0 -316
  50. package/src/orion/engine/core.py +0 -75
  51. package/src/orion/engine/crypto_guard/__pycache__/engine.cpython-313.pyc +0 -0
  52. package/src/orion/engine/crypto_guard/engine.py +0 -134
  53. package/src/orion/engine/data_residency/__pycache__/engine.cpython-313.pyc +0 -0
  54. package/src/orion/engine/data_residency/engine.py +0 -238
  55. package/src/orion/engine/dataflow/__pycache__/engine.cpython-313.pyc +0 -0
  56. package/src/orion/engine/dataflow/engine.py +0 -398
  57. package/src/orion/engine/dead_import/__pycache__/engine.cpython-313.pyc +0 -0
  58. package/src/orion/engine/dead_import/engine.py +0 -293
  59. package/src/orion/engine/deps/__pycache__/engine.cpython-313.pyc +0 -0
  60. package/src/orion/engine/deps/engine.py +0 -244
  61. package/src/orion/engine/dispatcher.py +0 -350
  62. package/src/orion/engine/docs/AUDIT_PROMPT.md +0 -1248
  63. package/src/orion/engine/duplication/__pycache__/engine.cpython-313.pyc +0 -0
  64. package/src/orion/engine/duplication/engine.py +0 -120
  65. package/src/orion/engine/error_handling/__pycache__/engine.cpython-313.pyc +0 -0
  66. package/src/orion/engine/error_handling/engine.py +0 -256
  67. package/src/orion/engine/fp_learn/__pycache__/engine.cpython-313.pyc +0 -0
  68. package/src/orion/engine/fp_learn/engine.py +0 -248
  69. package/src/orion/engine/hunter/__pycache__/engine.cpython-313.pyc +0 -0
  70. package/src/orion/engine/hunter/engine.py +0 -324
  71. package/src/orion/engine/iac_security/__pycache__/engine.cpython-313.pyc +0 -0
  72. package/src/orion/engine/iac_security/engine.py +0 -218
  73. package/src/orion/engine/ir.py +0 -22
  74. package/src/orion/engine/languages.py +0 -454
  75. package/src/orion/engine/license/__pycache__/engine.cpython-313.pyc +0 -0
  76. package/src/orion/engine/license/engine.py +0 -172
  77. package/src/orion/engine/lint/__pycache__/engine.cpython-313.pyc +0 -0
  78. package/src/orion/engine/lint/engine.py +0 -558
  79. package/src/orion/engine/llm_security/__pycache__/engine.cpython-313.pyc +0 -0
  80. package/src/orion/engine/llm_security/engine.py +0 -125
  81. package/src/orion/engine/logging_c/__pycache__/engine.cpython-313.pyc +0 -0
  82. package/src/orion/engine/logging_c/engine.py +0 -164
  83. package/src/orion/engine/noise/__pycache__/engine.cpython-313.pyc +0 -0
  84. package/src/orion/engine/noise/engine.py +0 -140
  85. package/src/orion/engine/orphan/__pycache__/engine.cpython-313.pyc +0 -0
  86. package/src/orion/engine/orphan/engine.py +0 -244
  87. package/src/orion/engine/perf_guard/__pycache__/engine.cpython-313.pyc +0 -0
  88. package/src/orion/engine/perf_guard/engine.py +0 -345
  89. package/src/orion/engine/prompt_security/__pycache__/engine.cpython-313.pyc +0 -0
  90. package/src/orion/engine/prompt_security/detectors/__init__.py +0 -4
  91. package/src/orion/engine/prompt_security/engine.py +0 -170
  92. package/src/orion/engine/provenance/__pycache__/engine.cpython-313.pyc +0 -0
  93. package/src/orion/engine/provenance/engine.py +0 -242
  94. package/src/orion/engine/registry.py +0 -55
  95. package/src/orion/engine/regression/__pycache__/engine.cpython-313.pyc +0 -0
  96. package/src/orion/engine/regression/engine.py +0 -213
  97. package/src/orion/engine/results.py +0 -21
  98. package/src/orion/engine/runner.py +0 -1310
  99. package/src/orion/engine/sandbox_escape/__pycache__/engine.cpython-313.pyc +0 -0
  100. package/src/orion/engine/sandbox_escape/engine.py +0 -284
  101. package/src/orion/engine/sast/__pycache__/engine.cpython-313.pyc +0 -0
  102. package/src/orion/engine/sast/engine.py +0 -217
  103. package/src/orion/engine/sast/patterns/__init__.py +0 -513
  104. package/src/orion/engine/sast/patterns/__pycache__/__init__.cpython-313.pyc +0 -0
  105. package/src/orion/engine/secrets/__pycache__/engine.cpython-313.pyc +0 -0
  106. package/src/orion/engine/secrets/engine.py +0 -77
  107. package/src/orion/engine/secrets/patterns/__init__.py +0 -74
  108. package/src/orion/engine/secrets/patterns/__pycache__/__init__.cpython-313.pyc +0 -0
  109. package/src/orion/engine/sensitive_data/__pycache__/engine.cpython-313.pyc +0 -0
  110. package/src/orion/engine/sensitive_data/engine.py +0 -143
  111. package/src/orion/engine/state/__pycache__/engine.cpython-313.pyc +0 -0
  112. package/src/orion/engine/state/engine.py +0 -218
  113. package/src/orion/engine/supply_chain_sbom/__pycache__/engine.cpython-313.pyc +0 -0
  114. package/src/orion/engine/supply_chain_sbom/engine.py +0 -290
  115. package/src/orion/engine/symbol/__pycache__/engine.cpython-313.pyc +0 -0
  116. package/src/orion/engine/symbol/engine.py +0 -463
  117. package/src/orion/engine/taint/__pycache__/engine.cpython-313.pyc +0 -0
  118. package/src/orion/engine/taint/engine.py +0 -464
  119. package/src/orion/engine/test_coverage/__pycache__/engine.cpython-313.pyc +0 -0
  120. package/src/orion/engine/test_coverage/engine.py +0 -445
  121. package/src/orion/engine/threat_model/__pycache__/engine.cpython-313.pyc +0 -0
  122. package/src/orion/engine/threat_model/engine.py +0 -126
  123. package/src/orion/engine/wiring_trace/__pycache__/engine.cpython-313.pyc +0 -0
  124. package/src/orion/engine/wiring_trace/engine.py +0 -200
  125. package/src/orion_engine.egg-info/PKG-INFO +0 -7
  126. package/src/orion_engine.egg-info/SOURCES.txt +0 -65
  127. package/src/orion_engine.egg-info/dependency_links.txt +0 -1
  128. package/src/orion_engine.egg-info/entry_points.txt +0 -2
  129. 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
- ))
@@ -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