@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,364 +0,0 @@
1
- """Config validation engine for YAML, JSON, TOML, .env, .ini, .properties, Python config files."""
2
- import ast
3
- import json
4
- import re
5
- import tomllib
6
- from pathlib import Path
7
- from orion.engine.config import EngineConfig
8
- from orion.engine.ir import OrionIR
9
- from orion.engine.results import EngineResult, Finding
10
-
11
-
12
- class Engine:
13
- name = "config_engine"
14
- version = "1.0.0"
15
-
16
- REQUIRED_KEYS = {
17
- "DATABASE_URL", "SECRET_KEY", "JWT_SECRET", "API_KEY",
18
- "REDIS_URL", "CELERY_BROKER_URL", "POSTGRES_URL", "MYSQL_URL",
19
- "MONGODB_URI", "RABBITMQ_URL", "KAFKA_BOOTSTRAP_SERVERS",
20
- "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "GOOGLE_API_KEY",
21
- "STRIPE_SECRET_KEY", "SENDGRID_API_KEY", "TWILIO_AUTH_TOKEN"
22
- }
23
-
24
- TYPE_HINTS = {
25
- "port": int, "PORT": int, "port_number": int,
26
- "debug": bool, "DEBUG": bool, "verbose": bool,
27
- "hosts": list, "HOSTS": list, "servers": list, "allowed_hosts": list,
28
- "timeout": (int, float), "TIMEOUT": (int, float),
29
- "retries": int, "RETRIES": int, "max_retries": int,
30
- "workers": int, "WORKERS": int,
31
- }
32
-
33
- PLACEHOLDER_PATTERNS = [
34
- r"changeme", r"your_.*_here", r"TODO", r"FIXME", r"xxx+",
35
- r"replace_me", r"insert_.*_here", r"<.*>", r"\{\{.*\}\}",
36
- r"your-.*-here", r"example\.com", r"localhost", r"127\.0\.0\.1"
37
- ]
38
-
39
- SENSITIVE_KEYS = {
40
- "SECRET_KEY", "JWT_SECRET", "API_KEY", "PASSWORD", "TOKEN",
41
- "PRIVATE_KEY", "ACCESS_TOKEN", "REFRESH_TOKEN", "CREDENTIALS"
42
- }
43
-
44
- def __init__(self):
45
- self._placeholder_regex = [re.compile(p, re.IGNORECASE) for p in self.PLACEHOLDER_PATTERNS]
46
-
47
- async def check_availability(self) -> bool:
48
- return True
49
-
50
- def resource_limits(self):
51
- from orion.engine.config import ResourceLimits
52
- return ResourceLimits(max_cpu_seconds=60, max_memory_mb=256)
53
-
54
- def _is_config_file(self, filename: str, ext: str) -> bool:
55
- if ext in (".yaml", ".yml", ".json", ".toml", ".env", ".ini", ".cfg", ".conf", ".properties"):
56
- return True
57
- if ext == ".py":
58
- name = filename.lower()
59
- return any(k in name for k in ("config", "settings", "conf", "env", "secret", "options"))
60
- return False
61
-
62
- async def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
63
- path = ir.metadata.get("path", "unknown")
64
- filename = Path(path).name.lower()
65
- ext = Path(path).suffix.lower()
66
-
67
- if not self._is_config_file(filename, ext):
68
- return EngineResult(engine=self.name, findings=[], ok=True)
69
-
70
- try:
71
- text = ir.content.decode("utf-8", errors="replace")
72
- except Exception:
73
- return EngineResult(engine=self.name, findings=[], ok=True)
74
-
75
- findings = []
76
- parsed = self._parse_config(text, filename, ext, path)
77
-
78
- if parsed:
79
- findings.extend(self._validate_config(parsed, path, filename, ext, config))
80
- findings.extend(self._check_secrets_in_config(parsed, path, filename, ext))
81
- findings.extend(self._check_duplicates(parsed, path, filename))
82
-
83
- return EngineResult(engine=self.name, findings=findings, ok=True)
84
-
85
- def _parse_config(self, text: str, filename: str, ext: str, path: str) -> dict | None:
86
- try:
87
- if ext in (".yaml", ".yml"):
88
- import yaml
89
- return yaml.safe_load(text) or {}
90
- elif ext == ".json":
91
- return json.loads(text) or {}
92
- elif ext == ".toml":
93
- return tomllib.loads(text) or {}
94
- elif ext in (".env",) or filename.startswith(".env"):
95
- return self._parse_env(text)
96
- elif ext in (".ini", ".cfg", ".conf"):
97
- return self._parse_ini(text)
98
- elif ext == ".properties":
99
- return self._parse_properties(text)
100
- elif ext == ".py" and self._is_config_file(filename, ext):
101
- return self._parse_python_config(text, path)
102
- except Exception:
103
- pass
104
- return None
105
-
106
- def _parse_env(self, text: str) -> dict:
107
- result = {}
108
- for line in text.splitlines():
109
- line = line.strip()
110
- if not line or line.startswith("#"):
111
- continue
112
- if "=" in line:
113
- key, value = line.split("=", 1)
114
- result[key.strip()] = value.strip().strip('"\'')
115
- return result
116
-
117
- def _parse_ini(self, text: str) -> dict:
118
- import configparser
119
- parser = configparser.ConfigParser()
120
- parser.read_string(text)
121
- result = {}
122
- for section in parser.sections():
123
- for key, value in parser[section].items():
124
- result[f"{section}.{key}"] = value
125
- return result
126
-
127
- def _parse_properties(self, text: str) -> dict:
128
- result = {}
129
- for line in text.splitlines():
130
- line = line.strip()
131
- if not line or line.startswith("#") or line.startswith("!"):
132
- continue
133
- if "=" in line:
134
- key, value = line.split("=", 1)
135
- result[key.strip()] = value.strip()
136
- elif ":" in line:
137
- key, value = line.split(":", 1)
138
- result[key.strip()] = value.strip()
139
- return result
140
-
141
- def _parse_python_config(self, text: str, path: str) -> dict:
142
- result = {}
143
- try:
144
- tree = ast.parse(text, filename=path)
145
- for node in ast.walk(tree):
146
- if isinstance(node, ast.Assign):
147
- for target in node.targets:
148
- if isinstance(target, ast.Name) and (target.id.isupper() or "_" in target.id):
149
- value = self._extract_value(node.value)
150
- if value is not None:
151
- result[target.id] = value
152
- except Exception:
153
- pass
154
- return result
155
-
156
- def _extract_value(self, node: ast.AST):
157
- if isinstance(node, ast.Constant):
158
- return node.value
159
- elif isinstance(node, ast.List):
160
- return [self._extract_value(e) for e in node.elts]
161
- elif isinstance(node, ast.Dict):
162
- return {self._extract_value(k): self._extract_value(v) for k, v in zip(node.keys, node.values)}
163
- elif isinstance(node, ast.Tuple):
164
- return tuple(self._extract_value(e) for e in node.elts)
165
- return None
166
-
167
- def _validate_config(self, config: dict, path: str, filename: str, ext: str, engine_config: EngineConfig) -> list[Finding]:
168
- findings = []
169
- flat = self._flatten_config(config)
170
-
171
- # Check explicitly configured required keys
172
- required_keys = engine_config.toggles.get("required_config_keys", [])
173
- for key in required_keys:
174
- if key not in flat and key.lower() not in {k.lower() for k in flat}:
175
- findings.append(Finding(
176
- engine=self.name,
177
- severity="high",
178
- title=f"Required config key missing: {key}",
179
- description=f"Expected configuration key '{key}' not found",
180
- location=path,
181
- metadata={
182
- "confidence": 0.85,
183
- "key": key,
184
- "expected_type": "str",
185
- "issue": "missing",
186
- "cwe": "CWE-256",
187
- },
188
- ))
189
-
190
- for key, value in flat.items():
191
- if isinstance(value, str):
192
- for pattern in self._placeholder_regex:
193
- if pattern.search(value):
194
- findings.append(Finding(
195
- engine=self.name,
196
- severity="high",
197
- title=f"Placeholder value in config: {key}",
198
- description=f"Config value appears to be a placeholder: {value[:50]}",
199
- location=path,
200
- metadata={
201
- "confidence": 0.9,
202
- "key": key,
203
- "issue": "placeholder_value",
204
- "value": value[:100],
205
- "cwe": "CWE-256",
206
- },
207
- ))
208
- break
209
-
210
- if self._looks_like_secret(key, value) and ext not in (".env",):
211
- findings.append(Finding(
212
- engine=self.name,
213
- severity="critical",
214
- title=f"Secret in non-.env config file: {key}",
215
- description=f"Sensitive value found in {ext} file (should be in .env)",
216
- location=path,
217
- metadata={
218
- "confidence": 0.95,
219
- "key": key,
220
- "issue": "secret_in_config",
221
- "cwe": "CWE-798",
222
- },
223
- ))
224
-
225
- expected_type = self.TYPE_HINTS.get(key)
226
- if expected_type and not self._type_matches(value, expected_type):
227
- findings.append(Finding(
228
- engine=self.name,
229
- severity="medium",
230
- title=f"Config type mismatch: {key}",
231
- description=f"Expected {expected_type.__name__ if hasattr(expected_type, '__name__') else expected_type}, got {type(value).__name__}",
232
- location=path,
233
- metadata={
234
- "confidence": 0.8,
235
- "key": key,
236
- "expected_type": str(expected_type),
237
- "actual_type": type(value).__name__,
238
- "issue": "type_mismatch",
239
- "cwe": "CWE-704",
240
- },
241
- ))
242
-
243
- if key.lower() in ("url", "endpoint", "uri") and isinstance(value, str):
244
- if not self._is_valid_url(value):
245
- findings.append(Finding(
246
- engine=self.name,
247
- severity="medium",
248
- title=f"Invalid URL format: {key}",
249
- description=f"Config value does not appear to be a valid URL: {value[:50]}",
250
- location=path,
251
- metadata={
252
- "confidence": 0.75,
253
- "key": key,
254
- "issue": "invalid_url",
255
- "value": value[:100],
256
- },
257
- ))
258
-
259
- if key.lower() in ("email", "mail") and isinstance(value, str):
260
- if not self._is_valid_email(value):
261
- findings.append(Finding(
262
- engine=self.name,
263
- severity="medium",
264
- title=f"Invalid email format: {key}",
265
- description=f"Config value does not appear to be a valid email: {value[:50]}",
266
- location=path,
267
- metadata={
268
- "confidence": 0.75,
269
- "key": key,
270
- "issue": "invalid_email",
271
- "value": value[:100],
272
- },
273
- ))
274
-
275
- return findings
276
-
277
- def _check_secrets_in_config(self, config: dict, path: str, filename: str, ext: str) -> list[Finding]:
278
- findings = []
279
- flat = self._flatten_config(config)
280
-
281
- for key, value in flat.items():
282
- if isinstance(value, str) and self._looks_like_secret(key, value):
283
- if ext not in (".env",) or "example" in filename or "template" in filename or "sample" in filename:
284
- findings.append(Finding(
285
- engine=self.name,
286
- severity="critical" if ext not in (".env",) else "high",
287
- title=f"Potential secret in config: {key}",
288
- description=f"Value for '{key}' matches secret patterns",
289
- location=path,
290
- metadata={
291
- "confidence": 0.85,
292
- "key": key,
293
- "issue": "potential_secret",
294
- "cwe": "CWE-798",
295
- },
296
- ))
297
- return findings
298
-
299
- def _check_duplicates(self, config: dict, path: str, filename: str) -> list[Finding]:
300
- findings = []
301
- flat = self._flatten_config(config)
302
- seen = {}
303
- for key in flat:
304
- lower_key = key.lower()
305
- if lower_key in seen:
306
- findings.append(Finding(
307
- engine=self.name,
308
- severity="medium",
309
- title=f"Duplicate config key (case-insensitive): {key}",
310
- description=f"Key '{key}' duplicates '{seen[lower_key]}' (case-insensitive)",
311
- location=path,
312
- metadata={
313
- "confidence": 0.9,
314
- "key": key,
315
- "duplicate_of": seen[lower_key],
316
- "issue": "duplicate_key",
317
- },
318
- ))
319
- else:
320
- seen[lower_key] = key
321
- return findings
322
-
323
- def _flatten_config(self, config: dict, prefix: str = "") -> dict:
324
- result = {}
325
- for key, value in config.items():
326
- full_key = f"{prefix}.{key}" if prefix else key
327
- if isinstance(value, dict):
328
- result.update(self._flatten_config(value, full_key))
329
- else:
330
- result[full_key] = value
331
- return result
332
-
333
- def _type_matches(self, value, expected_type) -> bool:
334
- if isinstance(expected_type, tuple):
335
- return isinstance(value, expected_type)
336
- if expected_type == int:
337
- return isinstance(value, int) and not isinstance(value, bool)
338
- if expected_type == bool:
339
- return isinstance(value, bool)
340
- if expected_type == list:
341
- return isinstance(value, list)
342
- if expected_type == float:
343
- return isinstance(value, (int, float))
344
- return isinstance(value, expected_type)
345
-
346
- def _looks_like_secret(self, key: str, value: str) -> bool:
347
- key_upper = key.upper()
348
- if any(sk in key_upper for sk in self.SENSITIVE_KEYS):
349
- if len(value) >= 16 and not value.startswith("${") and not value.startswith("{{"):
350
- return True
351
- return False
352
-
353
- def _is_valid_url(self, url: str) -> bool:
354
- url_pattern = re.compile(
355
- r'^(https?|ftp|postgres|mysql|redis|mongodb|amqp|kafka)://'
356
- r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|'
357
- r'localhost|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
358
- r'(?::\d+)?'
359
- r'(?:/?|[/?]\S+)$', re.IGNORECASE)
360
- return bool(url_pattern.match(url))
361
-
362
- def _is_valid_email(self, email: str) -> bool:
363
- email_pattern = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
364
- return bool(email_pattern.match(email))
@@ -1,313 +0,0 @@
1
- """Orion config self-validation engine - validates EngineConfig passed to run."""
2
- import re
3
- import os
4
- from pathlib import Path
5
- from typing import Dict, List, Any, Optional
6
-
7
- from orion.engine.config import EngineConfig, ResourceLimits, NetworkPolicy
8
- from orion.engine.ir import OrionIR
9
- from orion.engine.results import EngineResult, Finding
10
-
11
-
12
- class ConfigValidationEngine:
13
- name = "config_validation"
14
- version = "1.0.0"
15
-
16
- ALLOWED_OUTPUT_FORMATS = {"json", "sarif", "text", "html", "markdown", "csv"}
17
-
18
- async def check_availability(self) -> bool:
19
- return True
20
-
21
- def resource_limits(self):
22
- from orion.engine.config import ResourceLimits
23
- return ResourceLimits(max_cpu_seconds=5, max_memory_mb=32)
24
-
25
- def _validate_project_path(self, config: EngineConfig) -> List[Finding]:
26
- findings = []
27
- project_path = config.toggles.get("project_path")
28
- if not project_path:
29
- findings.append(Finding(
30
- engine=self.name,
31
- severity="error",
32
- title="Missing project_path in config",
33
- description="EngineConfig.toggles must contain 'project_path'",
34
- location="config.toggles.project_path",
35
- metadata={"rule": "missing_project_path"},
36
- ))
37
- else:
38
- path = Path(project_path)
39
- if not path.exists():
40
- findings.append(Finding(
41
- engine=self.name,
42
- severity="error",
43
- title=f"Project path does not exist: {project_path}",
44
- description="The specified project_path does not exist on filesystem",
45
- location="config.toggles.project_path",
46
- metadata={"rule": "project_path_not_found", "path": project_path},
47
- ))
48
- elif not path.is_dir():
49
- findings.append(Finding(
50
- engine=self.name,
51
- severity="error",
52
- title=f"Project path is not a directory: {project_path}",
53
- description="project_path must be a directory",
54
- location="config.toggles.project_path",
55
- metadata={"rule": "project_path_not_dir", "path": project_path},
56
- ))
57
- return findings
58
-
59
- def _validate_exclude_patterns(self, config: EngineConfig) -> List[Finding]:
60
- findings = []
61
- exclude = config.toggles.get("exclude", [])
62
- if not isinstance(exclude, list):
63
- findings.append(Finding(
64
- engine=self.name,
65
- severity="error",
66
- title="Invalid exclude configuration",
67
- description="config.toggles.exclude must be a list of glob patterns",
68
- location="config.toggles.exclude",
69
- metadata={"rule": "exclude_not_list"},
70
- ))
71
- return findings
72
-
73
- for i, pattern in enumerate(exclude):
74
- if not isinstance(pattern, str):
75
- findings.append(Finding(
76
- engine=self.name,
77
- severity="error",
78
- title=f"Exclude pattern {i} is not a string",
79
- description="All exclude patterns must be strings",
80
- location=f"config.toggles.exclude[{i}]",
81
- metadata={"rule": "exclude_pattern_not_string", "index": i},
82
- ))
83
- continue
84
-
85
- # Test if pattern is valid glob
86
- try:
87
- # Try to compile as a test
88
- Path("test").match(pattern)
89
- except Exception as e:
90
- findings.append(Finding(
91
- engine=self.name,
92
- severity="warning",
93
- title=f"Exclude pattern {i} may be invalid: {pattern}",
94
- description=f"Glob pattern validation failed: {e}",
95
- location=f"config.toggles.exclude[{i}]",
96
- metadata={"rule": "exclude_pattern_invalid", "index": i, "pattern": pattern, "error": str(e)},
97
- ))
98
- return findings
99
-
100
- def _validate_resource_limits(self, config: EngineConfig) -> List[Finding]:
101
- findings = []
102
- rl = config.resource_limits or {}
103
-
104
- # max_memory_mb
105
- memory = rl.get("max_memory_mb")
106
- if memory is not None:
107
- if not isinstance(memory, int) or memory <= 0:
108
- findings.append(Finding(
109
- engine=self.name,
110
- severity="error",
111
- title="Invalid max_memory_mb",
112
- description="resource_limits.max_memory_mb must be a positive integer",
113
- location="config.resource_limits.max_memory_mb",
114
- metadata={"rule": "invalid_max_memory", "value": memory},
115
- ))
116
-
117
- # max_cpu_seconds
118
- cpu = rl.get("max_cpu_seconds")
119
- if cpu is not None:
120
- if not isinstance(cpu, int) or cpu <= 0:
121
- findings.append(Finding(
122
- engine=self.name,
123
- severity="error",
124
- title="Invalid max_cpu_seconds",
125
- description="resource_limits.max_cpu_seconds must be a positive integer",
126
- location="config.resource_limits.max_cpu_seconds",
127
- metadata={"rule": "invalid_max_cpu", "value": cpu},
128
- ))
129
-
130
- # max_file_size_mb
131
- file_size = rl.get("max_file_size_mb")
132
- if file_size is not None:
133
- if not isinstance(file_size, int) or file_size <= 0:
134
- findings.append(Finding(
135
- engine=self.name,
136
- severity="error",
137
- title="Invalid max_file_size_mb",
138
- description="resource_limits.max_file_size_mb must be a positive integer",
139
- location="config.resource_limits.max_file_size_mb",
140
- metadata={"rule": "invalid_max_file_size", "value": file_size},
141
- ))
142
-
143
- return findings
144
-
145
- def _validate_network_policy(self, config: EngineConfig) -> List[Finding]:
146
- findings = []
147
- np = config.network_policy or {}
148
-
149
- allow_outbound = np.get("allow_outbound")
150
- if allow_outbound is not None and not isinstance(allow_outbound, bool):
151
- findings.append(Finding(
152
- engine=self.name,
153
- severity="error",
154
- title="Invalid network_policy.allow_outbound",
155
- description="network_policy.allow_outbound must be a boolean",
156
- location="config.network_policy.allow_outbound",
157
- metadata={"rule": "invalid_allow_outbound", "value": allow_outbound},
158
- ))
159
-
160
- allowed_hosts = np.get("allowed_hosts", [])
161
- if not isinstance(allowed_hosts, list):
162
- findings.append(Finding(
163
- engine=self.name,
164
- severity="error",
165
- title="Invalid network_policy.allowed_hosts",
166
- description="network_policy.allowed_hosts must be a list",
167
- location="config.network_policy.allowed_hosts",
168
- metadata={"rule": "invalid_allowed_hosts_type"},
169
- ))
170
- else:
171
- for i, host in enumerate(allowed_hosts):
172
- if not isinstance(host, str):
173
- findings.append(Finding(
174
- engine=self.name,
175
- severity="error",
176
- title=f"allowed_hosts[{i}] is not a string",
177
- description="All allowed_hosts entries must be strings",
178
- location=f"config.network_policy.allowed_hosts[{i}]",
179
- metadata={"rule": "allowed_host_not_string", "index": i},
180
- ))
181
-
182
- # Check if allow_outbound=False but allowed_hosts is not empty (contradiction)
183
- if allow_outbound is False and allowed_hosts:
184
- findings.append(Finding(
185
- engine=self.name,
186
- severity="warning",
187
- title="network_policy.allow_outbound is false but allowed_hosts is not empty",
188
- description="allowed_hosts will be ignored when allow_outbound is false",
189
- location="config.network_policy",
190
- metadata={"rule": "network_policy_contradiction"},
191
- ))
192
-
193
- return findings
194
-
195
- def _validate_parallelism(self, config: EngineConfig) -> List[Finding]:
196
- findings = []
197
- parallelism = config.toggles.get("parallelism")
198
- if parallelism is not None:
199
- if not isinstance(parallelism, int) or parallelism <= 0:
200
- findings.append(Finding(
201
- engine=self.name,
202
- severity="error",
203
- title="Invalid parallelism",
204
- description="parallelism must be a positive integer",
205
- location="config.toggles.parallelism",
206
- metadata={"rule": "invalid_parallelism", "value": parallelism},
207
- ))
208
- else:
209
- # Check against CPU count
210
- try:
211
- cpu_count = os.cpu_count() or 1
212
- if parallelism > cpu_count * 2:
213
- findings.append(Finding(
214
- engine=self.name,
215
- severity="warning",
216
- title=f"parallelism ({parallelism}) exceeds 2x CPU count ({cpu_count})",
217
- description="High parallelism may cause resource contention",
218
- location="config.toggles.parallelism",
219
- metadata={"rule": "parallelism_high", "parallelism": parallelism, "cpu_count": cpu_count},
220
- ))
221
- except Exception:
222
- pass
223
- return findings
224
-
225
- def _validate_timeout(self, config: EngineConfig) -> List[Finding]:
226
- findings = []
227
- timeout = config.toggles.get("timeout")
228
- if timeout is not None:
229
- if not isinstance(timeout, (int, float)) or timeout <= 0:
230
- findings.append(Finding(
231
- engine=self.name,
232
- severity="error",
233
- title="Invalid timeout",
234
- description="timeout must be a positive number",
235
- location="config.toggles.timeout",
236
- metadata={"rule": "invalid_timeout", "value": timeout},
237
- ))
238
- return findings
239
-
240
- def _validate_output_format(self, config: EngineConfig) -> List[Finding]:
241
- findings = []
242
- output_format = config.toggles.get("output_format")
243
- if output_format is not None:
244
- if not isinstance(output_format, str):
245
- findings.append(Finding(
246
- engine=self.name,
247
- severity="error",
248
- title="Invalid output_format",
249
- description="output_format must be a string",
250
- location="config.toggles.output_format",
251
- metadata={"rule": "invalid_output_format_type"},
252
- ))
253
- elif output_format not in self.ALLOWED_OUTPUT_FORMATS:
254
- findings.append(Finding(
255
- engine=self.name,
256
- severity="warning",
257
- title=f"Unknown output_format: {output_format}",
258
- description=f"Allowed formats: {', '.join(sorted(self.ALLOWED_OUTPUT_FORMATS))}",
259
- location="config.toggles.output_format",
260
- metadata={"rule": "unknown_output_format", "value": output_format, "allowed": list(self.ALLOWED_OUTPUT_FORMATS)},
261
- ))
262
- return findings
263
-
264
- def _validate_engine_toggles(self, config: EngineConfig) -> List[Finding]:
265
- findings = []
266
- # Check for unknown engine toggles
267
- known_toggles = {
268
- "project_path", "exclude", "parallelism", "timeout", "output_format",
269
- "fail_on_severity", "include_tests", "include_generated",
270
- }
271
- for key in config.toggles:
272
- if key not in known_toggles and not key.startswith("_"):
273
- findings.append(Finding(
274
- engine=self.name,
275
- severity="warning",
276
- title=f"Unknown config toggle: {key}",
277
- description=f"Toggle '{key}' is not a recognized Orion configuration option",
278
- location=f"config.toggles.{key}",
279
- metadata={"rule": "unknown_toggle", "toggle": key},
280
- ))
281
- return findings
282
-
283
- async def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
284
- all_findings = []
285
-
286
- # Run all validations
287
- all_findings.extend(self._validate_project_path(config))
288
- all_findings.extend(self._validate_exclude_patterns(config))
289
- all_findings.extend(self._validate_resource_limits(config))
290
- all_findings.extend(self._validate_network_policy(config))
291
- all_findings.extend(self._validate_parallelism(config))
292
- all_findings.extend(self._validate_timeout(config))
293
- all_findings.extend(self._validate_output_format(config))
294
- all_findings.extend(self._validate_engine_toggles(config))
295
-
296
- # Count errors and warnings
297
- errors = sum(1 for f in all_findings if f.severity == "error")
298
- warnings = sum(1 for f in all_findings if f.severity == "warning")
299
-
300
- return EngineResult(
301
- engine=self.name,
302
- findings=all_findings,
303
- ok=errors == 0,
304
- metrics={
305
- "validations_run": 7,
306
- "errors": errors,
307
- "warnings": warnings,
308
- "total_findings": len(all_findings),
309
- },
310
- )
311
-
312
-
313
- Engine = ConfigValidationEngine