@orionscanner/cli 0.1.0 → 0.1.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 +135 -248
- package/bin/orion.js +30 -29
- package/dist/orion.exe +0 -0
- package/package.json +8 -10
- 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,463 +0,0 @@
|
|
|
1
|
-
"""Architecture violations engine - detects structural issues in codebase."""
|
|
2
|
-
import ast
|
|
3
|
-
import os
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Dict, List, Set, Optional, Tuple
|
|
6
|
-
from collections import defaultdict, deque
|
|
7
|
-
|
|
8
|
-
from orion.engine.config import EngineConfig
|
|
9
|
-
from orion.engine.ir import OrionIR
|
|
10
|
-
from orion.engine.results import EngineResult, Finding
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class ArchitectureEngine:
|
|
14
|
-
name = "architecture"
|
|
15
|
-
version = "1.0.0"
|
|
16
|
-
|
|
17
|
-
LAYER_ORDER = ["domain", "application", "infrastructure", "presentation"]
|
|
18
|
-
LAYER_PATTERNS = {
|
|
19
|
-
"domain": ["domain", "entities", "value_objects", "aggregates", "events", "specifications"],
|
|
20
|
-
"application": ["application", "use_cases", "services", "commands", "queries", "dtos", "ports"],
|
|
21
|
-
"infrastructure": ["infrastructure", "repositories", "persistence", "external", "adapters", "gateways", "orm", "database", "migrations"],
|
|
22
|
-
"presentation": ["presentation", "api", "controllers", "views", "handlers", "endpoints", "routes", "graphql", "rest", "cli", "web", "ui"],
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
EXCLUDED_DIRS = {"__pycache__", ".git", ".venv", "venv", "env", "node_modules", "dist", "build", "tests", "test", "scripts", "migrations"}
|
|
26
|
-
|
|
27
|
-
async def check_availability(self) -> bool:
|
|
28
|
-
return True
|
|
29
|
-
|
|
30
|
-
def resource_limits(self):
|
|
31
|
-
from orion.engine.config import ResourceLimits
|
|
32
|
-
return ResourceLimits(max_cpu_seconds=60, max_memory_mb=256)
|
|
33
|
-
|
|
34
|
-
def _should_exclude(self, filepath: str, config: EngineConfig) -> bool:
|
|
35
|
-
path = Path(filepath)
|
|
36
|
-
for part in path.parts:
|
|
37
|
-
if part in self.EXCLUDED_DIRS:
|
|
38
|
-
return True
|
|
39
|
-
if config.toggles.get("exclude"):
|
|
40
|
-
for pattern in config.toggles["exclude"]:
|
|
41
|
-
if path.match(pattern) or any(p.match(pattern) for p in path.parents):
|
|
42
|
-
return True
|
|
43
|
-
return False
|
|
44
|
-
|
|
45
|
-
def _is_python_file(self, filepath: str) -> bool:
|
|
46
|
-
return Path(filepath).suffix == ".py"
|
|
47
|
-
|
|
48
|
-
def _get_layer(self, filepath: str) -> Optional[str]:
|
|
49
|
-
"""Determine architectural layer from file path."""
|
|
50
|
-
path_str = str(Path(filepath)).replace('\\', '/').lower()
|
|
51
|
-
for layer, patterns in self.LAYER_PATTERNS.items():
|
|
52
|
-
for pattern in patterns:
|
|
53
|
-
if f"/{pattern}/" in path_str or path_str.startswith(f"{pattern}/"):
|
|
54
|
-
return layer
|
|
55
|
-
return None
|
|
56
|
-
|
|
57
|
-
def _get_module_name(self, filepath: str, project_root: str) -> Optional[str]:
|
|
58
|
-
try:
|
|
59
|
-
path = Path(filepath).relative_to(Path(project_root))
|
|
60
|
-
except ValueError:
|
|
61
|
-
return None
|
|
62
|
-
|
|
63
|
-
if path.suffix == ".py":
|
|
64
|
-
path = path.with_suffix("")
|
|
65
|
-
|
|
66
|
-
parts = list(path.parts)
|
|
67
|
-
if parts and parts[-1] == "__init__":
|
|
68
|
-
parts = parts[:-1]
|
|
69
|
-
|
|
70
|
-
return ".".join(parts) if parts else None
|
|
71
|
-
|
|
72
|
-
def _extract_imports(self, content: str) -> List[str]:
|
|
73
|
-
imports = []
|
|
74
|
-
try:
|
|
75
|
-
tree = ast.parse(content)
|
|
76
|
-
for node in ast.walk(tree):
|
|
77
|
-
if isinstance(node, ast.Import):
|
|
78
|
-
for alias in node.names:
|
|
79
|
-
imports.append(alias.name.split('.')[0])
|
|
80
|
-
elif isinstance(node, ast.ImportFrom):
|
|
81
|
-
if node.module:
|
|
82
|
-
imports.append(node.module.split('.')[0])
|
|
83
|
-
except SyntaxError:
|
|
84
|
-
pass
|
|
85
|
-
return imports
|
|
86
|
-
|
|
87
|
-
def _detect_cycles(self, graph: Dict[str, Set[str]]) -> List[List[str]]:
|
|
88
|
-
"""Detect cycles in directed graph using Tarjan's algorithm."""
|
|
89
|
-
index = 0
|
|
90
|
-
stack = []
|
|
91
|
-
on_stack = set()
|
|
92
|
-
indices = {}
|
|
93
|
-
lowlinks = {}
|
|
94
|
-
cycles = []
|
|
95
|
-
|
|
96
|
-
def strongconnect(v: str):
|
|
97
|
-
nonlocal index
|
|
98
|
-
indices[v] = index
|
|
99
|
-
lowlinks[v] = index
|
|
100
|
-
index += 1
|
|
101
|
-
stack.append(v)
|
|
102
|
-
on_stack.add(v)
|
|
103
|
-
|
|
104
|
-
for w in graph.get(v, set()):
|
|
105
|
-
if w not in indices:
|
|
106
|
-
strongconnect(w)
|
|
107
|
-
lowlinks[v] = min(lowlinks[v], lowlinks[w])
|
|
108
|
-
elif w in on_stack:
|
|
109
|
-
lowlinks[v] = min(lowlinks[v], indices[w])
|
|
110
|
-
|
|
111
|
-
if lowlinks[v] == indices[v]:
|
|
112
|
-
scc = []
|
|
113
|
-
while True:
|
|
114
|
-
w = stack.pop()
|
|
115
|
-
on_stack.remove(w)
|
|
116
|
-
scc.append(w)
|
|
117
|
-
if w == v:
|
|
118
|
-
break
|
|
119
|
-
if len(scc) > 1:
|
|
120
|
-
cycles.append(scc)
|
|
121
|
-
|
|
122
|
-
for v in graph:
|
|
123
|
-
if v not in indices:
|
|
124
|
-
strongconnect(v)
|
|
125
|
-
|
|
126
|
-
return cycles
|
|
127
|
-
|
|
128
|
-
def _find_cycle_paths(self, graph: Dict[str, Set[str]], cycles: List[List[str]]) -> List[List[str]]:
|
|
129
|
-
"""Find actual paths for each cycle."""
|
|
130
|
-
paths = []
|
|
131
|
-
for cycle in cycles:
|
|
132
|
-
# Find a path that forms the cycle
|
|
133
|
-
start = cycle[0]
|
|
134
|
-
visited = set()
|
|
135
|
-
path = []
|
|
136
|
-
|
|
137
|
-
def dfs(node, target):
|
|
138
|
-
if node in visited:
|
|
139
|
-
return False
|
|
140
|
-
visited.add(node)
|
|
141
|
-
path.append(node)
|
|
142
|
-
if node == target and len(path) > 1:
|
|
143
|
-
return True
|
|
144
|
-
for neighbor in graph.get(node, set()):
|
|
145
|
-
if neighbor in cycle:
|
|
146
|
-
if dfs(neighbor, target):
|
|
147
|
-
return True
|
|
148
|
-
path.pop()
|
|
149
|
-
return False
|
|
150
|
-
|
|
151
|
-
dfs(start, start)
|
|
152
|
-
if path:
|
|
153
|
-
paths.append(path)
|
|
154
|
-
return paths
|
|
155
|
-
|
|
156
|
-
def _check_layer_violations(self, module_layers: Dict[str, str], imports_map: Dict[str, Set[str]]) -> List[Finding]:
|
|
157
|
-
findings = []
|
|
158
|
-
layer_index = {layer: i for i, layer in enumerate(self.LAYER_ORDER)}
|
|
159
|
-
|
|
160
|
-
for importer, imported_set in imports_map.items():
|
|
161
|
-
importer_layer = module_layers.get(importer)
|
|
162
|
-
if not importer_layer:
|
|
163
|
-
continue
|
|
164
|
-
|
|
165
|
-
importer_idx = layer_index.get(importer_layer, 999)
|
|
166
|
-
|
|
167
|
-
for imported in imported_set:
|
|
168
|
-
imported_layer = module_layers.get(imported)
|
|
169
|
-
if not imported_layer:
|
|
170
|
-
continue
|
|
171
|
-
|
|
172
|
-
imported_idx = layer_index.get(imported_layer, 999)
|
|
173
|
-
|
|
174
|
-
# Check for violations: LOWER layer importing HIGHER layer
|
|
175
|
-
# domain (0) should not import application (1), infrastructure (2), presentation (3)
|
|
176
|
-
# application (1) should not import infrastructure (2), presentation (3)
|
|
177
|
-
# infrastructure (2) should not import presentation (3)
|
|
178
|
-
if importer_idx < imported_idx:
|
|
179
|
-
# VIOLATION: lower layer importing higher layer
|
|
180
|
-
violation_type = ""
|
|
181
|
-
if importer_layer == "domain" and imported_layer in ("application", "infrastructure", "presentation"):
|
|
182
|
-
violation_type = "domain_imports_higher_layer"
|
|
183
|
-
elif importer_layer == "application" and imported_layer in ("infrastructure", "presentation"):
|
|
184
|
-
violation_type = "application_imports_higher_layer"
|
|
185
|
-
elif importer_layer == "infrastructure" and imported_layer == "presentation":
|
|
186
|
-
violation_type = "infrastructure_imports_presentation"
|
|
187
|
-
else:
|
|
188
|
-
violation_type = "layer_violation"
|
|
189
|
-
|
|
190
|
-
findings.append(Finding(
|
|
191
|
-
engine=self.name,
|
|
192
|
-
severity="high",
|
|
193
|
-
title=f"Layer violation: {importer_layer} imports {imported_layer}",
|
|
194
|
-
description=f"Module '{importer}' (layer: {importer_layer}) imports '{imported}' (layer: {imported_layer}) - violates layered architecture",
|
|
195
|
-
location=importer,
|
|
196
|
-
metadata={
|
|
197
|
-
"violation": violation_type,
|
|
198
|
-
"importer": importer,
|
|
199
|
-
"importer_layer": importer_layer,
|
|
200
|
-
"imported": imported,
|
|
201
|
-
"imported_layer": imported_layer,
|
|
202
|
-
},
|
|
203
|
-
))
|
|
204
|
-
return findings
|
|
205
|
-
|
|
206
|
-
def _check_god_modules(self, module_to_file: Dict[str, str], all_files: Dict[str, OrionIR]) -> List[Finding]:
|
|
207
|
-
findings = []
|
|
208
|
-
for module, filepath in module_to_file.items():
|
|
209
|
-
file_ir = all_files.get(filepath)
|
|
210
|
-
if not file_ir:
|
|
211
|
-
continue
|
|
212
|
-
try:
|
|
213
|
-
content = file_ir.content.decode("utf-8", errors="replace")
|
|
214
|
-
lines = content.splitlines()
|
|
215
|
-
if len(lines) > 1000:
|
|
216
|
-
findings.append(Finding(
|
|
217
|
-
engine=self.name,
|
|
218
|
-
severity="medium",
|
|
219
|
-
title=f"God module: {module} has {len(lines)} lines",
|
|
220
|
-
description=f"Module exceeds 1000 lines ({len(lines)}), consider splitting",
|
|
221
|
-
location=filepath,
|
|
222
|
-
metadata={
|
|
223
|
-
"violation": "god_module",
|
|
224
|
-
"module": module,
|
|
225
|
-
"file": filepath,
|
|
226
|
-
"line_count": len(lines),
|
|
227
|
-
},
|
|
228
|
-
))
|
|
229
|
-
except Exception:
|
|
230
|
-
pass
|
|
231
|
-
return findings
|
|
232
|
-
|
|
233
|
-
def _check_feature_envy(self, all_files: Dict[str, OrionIR], module_to_file: Dict[str, str], project_root: str) -> List[Finding]:
|
|
234
|
-
findings = []
|
|
235
|
-
# Feature envy: method uses more attributes of another class than its own
|
|
236
|
-
# This is a simplified check - we look for methods that heavily use another class
|
|
237
|
-
for module, filepath in module_to_file.items():
|
|
238
|
-
file_ir = all_files.get(filepath)
|
|
239
|
-
if not file_ir:
|
|
240
|
-
continue
|
|
241
|
-
try:
|
|
242
|
-
content = file_ir.content.decode("utf-8", errors="replace")
|
|
243
|
-
tree = ast.parse(content)
|
|
244
|
-
except Exception:
|
|
245
|
-
continue
|
|
246
|
-
|
|
247
|
-
for node in ast.walk(tree):
|
|
248
|
-
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
249
|
-
# Count attribute accesses
|
|
250
|
-
attr_accesses = defaultdict(int)
|
|
251
|
-
for n in ast.walk(node):
|
|
252
|
-
if isinstance(n, ast.Attribute) and isinstance(n.ctx, ast.Load):
|
|
253
|
-
# Get the base object name
|
|
254
|
-
if isinstance(n.value, ast.Name):
|
|
255
|
-
attr_accesses[n.value.id] += 1
|
|
256
|
-
|
|
257
|
-
if attr_accesses:
|
|
258
|
-
# Find the most accessed object
|
|
259
|
-
max_obj = max(attr_accesses, key=attr_accesses.get)
|
|
260
|
-
max_count = attr_accesses[max_obj]
|
|
261
|
-
total = sum(attr_accesses.values())
|
|
262
|
-
|
|
263
|
-
# If more than 60% of attribute accesses are to another object
|
|
264
|
-
if max_count > 3 and max_count / total > 0.6 and max_obj != "self":
|
|
265
|
-
findings.append(Finding(
|
|
266
|
-
engine=self.name,
|
|
267
|
-
severity="low",
|
|
268
|
-
title=f"Possible feature envy in {node.name}",
|
|
269
|
-
description=f"Method '{node.name}' accesses '{max_obj}' {max_count} times out of {total} total attribute accesses",
|
|
270
|
-
location=f"{filepath}:{node.lineno}",
|
|
271
|
-
metadata={
|
|
272
|
-
"violation": "feature_envy",
|
|
273
|
-
"function": node.name,
|
|
274
|
-
"file": filepath,
|
|
275
|
-
"envied_object": max_obj,
|
|
276
|
-
"access_count": max_count,
|
|
277
|
-
"total_accesses": total,
|
|
278
|
-
},
|
|
279
|
-
))
|
|
280
|
-
return findings
|
|
281
|
-
|
|
282
|
-
def _check_data_class_behavior(self, all_files: Dict[str, OrionIR], module_to_file: Dict[str, str]) -> List[Finding]:
|
|
283
|
-
findings = []
|
|
284
|
-
for module, filepath in module_to_file.items():
|
|
285
|
-
file_ir = all_files.get(filepath)
|
|
286
|
-
if not file_ir:
|
|
287
|
-
continue
|
|
288
|
-
try:
|
|
289
|
-
content = file_ir.content.decode("utf-8", errors="replace")
|
|
290
|
-
tree = ast.parse(content)
|
|
291
|
-
except Exception:
|
|
292
|
-
continue
|
|
293
|
-
|
|
294
|
-
for node in ast.walk(tree):
|
|
295
|
-
if isinstance(node, ast.ClassDef):
|
|
296
|
-
# Check if it looks like a data class (has __dataclass_fields__ or @dataclass)
|
|
297
|
-
is_dataclass = False
|
|
298
|
-
for decorator in node.decorator_list:
|
|
299
|
-
if isinstance(decorator, ast.Name) and decorator.id == "dataclass":
|
|
300
|
-
is_dataclass = True
|
|
301
|
-
elif isinstance(decorator, ast.Attribute) and decorator.attr == "dataclass":
|
|
302
|
-
is_dataclass = True
|
|
303
|
-
|
|
304
|
-
if is_dataclass:
|
|
305
|
-
methods = [n for n in node.body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
|
|
306
|
-
allowed = {"__init__", "__post_init__", "__repr__", "__eq__", "__hash__", "__str__", "__lt__", "__le__", "__gt__", "__ge__"}
|
|
307
|
-
extra_methods = [m.name for m in methods if m.name not in allowed and not m.name.startswith('_')]
|
|
308
|
-
if extra_methods:
|
|
309
|
-
findings.append(Finding(
|
|
310
|
-
engine=self.name,
|
|
311
|
-
severity="low",
|
|
312
|
-
title=f"Data class with behavior: {node.name}",
|
|
313
|
-
description=f"Data class has methods beyond data protocols: {', '.join(extra_methods)}",
|
|
314
|
-
location=f"{filepath}:{node.lineno}",
|
|
315
|
-
metadata={
|
|
316
|
-
"violation": "data_class_with_behavior",
|
|
317
|
-
"class": node.name,
|
|
318
|
-
"file": filepath,
|
|
319
|
-
"methods": extra_methods,
|
|
320
|
-
},
|
|
321
|
-
))
|
|
322
|
-
return findings
|
|
323
|
-
|
|
324
|
-
def _check_circular_package_deps(self, module_to_file: Dict[str, str], imports_map: Dict[str, Set[str]]) -> List[Finding]:
|
|
325
|
-
"""Check for circular dependencies at package level."""
|
|
326
|
-
findings = []
|
|
327
|
-
# Group modules by package (top-level)
|
|
328
|
-
package_map = defaultdict(set)
|
|
329
|
-
for module in module_to_file:
|
|
330
|
-
top_level = module.split('.')[0] if '.' in module else module
|
|
331
|
-
package_map[top_level].add(module)
|
|
332
|
-
|
|
333
|
-
# Build package-level graph
|
|
334
|
-
package_graph = defaultdict(set)
|
|
335
|
-
for importer, imported_set in imports_map.items():
|
|
336
|
-
importer_pkg = importer.split('.')[0] if '.' in importer else importer
|
|
337
|
-
for imported in imported_set:
|
|
338
|
-
imported_pkg = imported.split('.')[0] if '.' in imported else imported
|
|
339
|
-
if importer_pkg != imported_pkg:
|
|
340
|
-
package_graph[importer_pkg].add(imported_pkg)
|
|
341
|
-
|
|
342
|
-
# Detect cycles at package level
|
|
343
|
-
cycles = self._detect_cycles(package_graph)
|
|
344
|
-
for cycle in cycles:
|
|
345
|
-
findings.append(Finding(
|
|
346
|
-
engine=self.name,
|
|
347
|
-
severity="high",
|
|
348
|
-
title=f"Circular package dependency: {' -> '.join(cycle)}",
|
|
349
|
-
description=f"Packages form a circular dependency",
|
|
350
|
-
location=cycle[0],
|
|
351
|
-
metadata={
|
|
352
|
-
"violation": "circular_package_dependency",
|
|
353
|
-
"packages": cycle,
|
|
354
|
-
},
|
|
355
|
-
))
|
|
356
|
-
return findings
|
|
357
|
-
|
|
358
|
-
async def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
|
|
359
|
-
project_root = ir.metadata.get("project_root", ".")
|
|
360
|
-
all_files = ir.metadata.get("all_files", {})
|
|
361
|
-
|
|
362
|
-
if not all_files:
|
|
363
|
-
return EngineResult(engine=self.name, ok=True, metrics={"mode": "single_file"})
|
|
364
|
-
|
|
365
|
-
# Build module map
|
|
366
|
-
module_to_file = {}
|
|
367
|
-
file_to_module = {}
|
|
368
|
-
module_layers = {}
|
|
369
|
-
|
|
370
|
-
for filepath, file_ir in all_files.items():
|
|
371
|
-
if not self._is_python_file(filepath):
|
|
372
|
-
continue
|
|
373
|
-
if self._should_exclude(filepath, config):
|
|
374
|
-
continue
|
|
375
|
-
|
|
376
|
-
module_name = self._get_module_name(filepath, project_root)
|
|
377
|
-
if not module_name:
|
|
378
|
-
continue
|
|
379
|
-
|
|
380
|
-
module_to_file[module_name] = filepath
|
|
381
|
-
file_to_module[filepath] = module_name
|
|
382
|
-
layer = self._get_layer(filepath)
|
|
383
|
-
if layer:
|
|
384
|
-
module_layers[module_name] = layer
|
|
385
|
-
|
|
386
|
-
# Build import graph
|
|
387
|
-
imports_map = defaultdict(set)
|
|
388
|
-
for filepath, file_ir in all_files.items():
|
|
389
|
-
if not self._is_python_file(filepath):
|
|
390
|
-
continue
|
|
391
|
-
if self._should_exclude(filepath, config):
|
|
392
|
-
continue
|
|
393
|
-
|
|
394
|
-
module_name = file_to_module.get(filepath)
|
|
395
|
-
if not module_name:
|
|
396
|
-
continue
|
|
397
|
-
|
|
398
|
-
try:
|
|
399
|
-
content = file_ir.content.decode("utf-8", errors="replace")
|
|
400
|
-
except Exception:
|
|
401
|
-
continue
|
|
402
|
-
|
|
403
|
-
imports = self._extract_imports(content)
|
|
404
|
-
for imp in imports:
|
|
405
|
-
# Resolve to known modules
|
|
406
|
-
if imp in module_to_file:
|
|
407
|
-
imports_map[module_name].add(imp)
|
|
408
|
-
else:
|
|
409
|
-
# Try to match partial
|
|
410
|
-
for known_module in module_to_file:
|
|
411
|
-
if known_module.startswith(imp + ".") or imp.startswith(known_module + "."):
|
|
412
|
-
imports_map[module_name].add(known_module)
|
|
413
|
-
break
|
|
414
|
-
|
|
415
|
-
all_findings = []
|
|
416
|
-
|
|
417
|
-
# 1. Cyclic imports (module level)
|
|
418
|
-
cycles = self._detect_cycles(imports_map)
|
|
419
|
-
cycle_paths = self._find_cycle_paths(imports_map, cycles)
|
|
420
|
-
for path in cycle_paths:
|
|
421
|
-
all_findings.append(Finding(
|
|
422
|
-
engine=self.name,
|
|
423
|
-
severity="high",
|
|
424
|
-
title=f"Cyclic import: {' -> '.join(path)}",
|
|
425
|
-
description=f"Modules form a circular import dependency",
|
|
426
|
-
location=path[0],
|
|
427
|
-
metadata={
|
|
428
|
-
"violation": "cyclic_import",
|
|
429
|
-
"modules": path,
|
|
430
|
-
"files": [module_to_file.get(m, "") for m in path],
|
|
431
|
-
},
|
|
432
|
-
))
|
|
433
|
-
|
|
434
|
-
# 2. Layer violations
|
|
435
|
-
all_findings.extend(self._check_layer_violations(module_layers, imports_map))
|
|
436
|
-
|
|
437
|
-
# 3. God modules
|
|
438
|
-
all_findings.extend(self._check_god_modules(module_to_file, all_files))
|
|
439
|
-
|
|
440
|
-
# 4. Feature envy
|
|
441
|
-
all_findings.extend(self._check_feature_envy(all_files, module_to_file, project_root))
|
|
442
|
-
|
|
443
|
-
# 5. Data class with behavior
|
|
444
|
-
all_findings.extend(self._check_data_class_behavior(all_files, module_to_file))
|
|
445
|
-
|
|
446
|
-
# 6. Circular package dependencies
|
|
447
|
-
all_findings.extend(self._check_circular_package_deps(module_to_file, imports_map))
|
|
448
|
-
|
|
449
|
-
return EngineResult(
|
|
450
|
-
engine=self.name,
|
|
451
|
-
findings=all_findings,
|
|
452
|
-
ok=True,
|
|
453
|
-
metrics={
|
|
454
|
-
"modules_analyzed": len(module_to_file),
|
|
455
|
-
"import_edges": sum(len(v) for v in imports_map.values()),
|
|
456
|
-
"cycles_found": len(cycles),
|
|
457
|
-
"layers_detected": len(set(module_layers.values())),
|
|
458
|
-
"findings_count": len(all_findings),
|
|
459
|
-
},
|
|
460
|
-
)
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
Engine = ArchitectureEngine
|
|
Binary file
|