@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,125 +0,0 @@
1
- """AI & Large Language Model (LLM) Agent Security Engine.
2
-
3
- 100% Custom In-House Developed Engine for Orion Engine Platform (OWASP Top 10 for LLMs).
4
- Detects prompt injection vectors, unsafe output execution, system prompt leaks, and excessive agent autonomy.
5
- """
6
- import re
7
- from pathlib import Path
8
- from typing import List, Dict, Any
9
-
10
- from orion.engine.config import EngineConfig, ResourceLimits
11
- from orion.engine.ir import OrionIR
12
- from orion.engine.results import EngineResult, Finding
13
-
14
-
15
- LLM_RULES = [
16
- {
17
- "id": "LLM-001",
18
- "pattern": r"(?i)\b(eval|exec|subprocess\.run|os\.system|child_process\.exec)\s*\([^)]*(response|output|completion|llm_out|generation|text)\b",
19
- "title": "Unsafe execution of LLM output (LLM02: Insecure Output Handling)",
20
- "description": "Directly evaluating or executing untrusted LLM-generated code or commands can lead to remote code execution",
21
- "severity": "critical",
22
- "cwe": "CWE-94",
23
- "confidence": 0.9,
24
- },
25
- {
26
- "id": "LLM-002",
27
- "pattern": r'(?i)(f["\'].*\{.*(user_input|prompt|query|input_text|req\.body|message).*\}.*|format\s*\([^)]*(user_input|prompt|query|input_text)\)|template\s*\+\s*(user_input|query|prompt))',
28
- "title": "Unsanitized direct string interpolation into system prompt (LLM01: Prompt Injection)",
29
- "description": "User input is directly concatenated into prompt templates without boundary tags or guardrail filtering",
30
- "severity": "high",
31
- "cwe": "CWE-20",
32
- "confidence": 0.8,
33
- },
34
- {
35
- "id": "LLM-003",
36
- "pattern": r'(?i)(system_prompt|system_instruction)\s*[:=]\s*["\'][^"\']*(ignore previous instructions|you are a|classified|secret|internal only)',
37
- "title": "Hardcoded sensitive system prompt with confidential constraints",
38
- "description": "Hardcoded system prompts often leak via direct user query or jailbreak techniques (LLM06: Sensitive Information Disclosure)",
39
- "severity": "medium",
40
- "cwe": "CWE-200",
41
- "confidence": 0.85,
42
- },
43
- {
44
- "id": "LLM-004",
45
- "pattern": r'(?i)(auto_approve\s*=\s*True|allow_all_tools\s*=\s*True|autonomous_execution\s*=\s*True|dangerously_allow_execution\s*=\s*True)',
46
- "title": "Excessive agent tool autonomy enabled without human-in-the-loop (LLM08: Excessive Agency)",
47
- "description": "AI Agent configured to execute destructive tools or modifications without confirmation gates",
48
- "severity": "high",
49
- "cwe": "CWE-863",
50
- "confidence": 0.9,
51
- },
52
- {
53
- "id": "LLM-005",
54
- "pattern": r'(?i)(openai\.api_key|anthropic_api_key|gemini_api_key|groq_api_key)\s*=\s*["\'][a-zA-Z0-9_\-]{20,}["\']',
55
- "title": "Hardcoded LLM provider API credentials",
56
- "description": "Hardcoded AI model API key detected in source code. Store credentials in secure secret managers",
57
- "severity": "critical",
58
- "cwe": "CWE-798",
59
- "confidence": 0.95,
60
- },
61
- ]
62
-
63
-
64
- class LlmSecurityEngine:
65
- """100% custom, in-house developed AI & LLM Agent Security Engine."""
66
-
67
- name = "llm_security"
68
- version = "1.0.0"
69
-
70
- def __init__(self):
71
- self._compiled_rules = []
72
- for r in LLM_RULES:
73
- self._compiled_rules.append({
74
- **r,
75
- "regex": re.compile(r["pattern"])
76
- })
77
-
78
- async def check_availability(self) -> bool:
79
- return True
80
-
81
- def resource_limits(self) -> ResourceLimits:
82
- return ResourceLimits(max_cpu_seconds=30, max_memory_mb=256)
83
-
84
- async def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
85
- path = ir.metadata.get("path", "unknown")
86
- try:
87
- content = ir.content.decode("utf-8", errors="replace")
88
- except Exception:
89
- return EngineResult(engine=self.name, findings=[], ok=True)
90
-
91
- lines = content.splitlines()
92
- findings = []
93
-
94
- for rule in self._compiled_rules:
95
- for match in rule["regex"].finditer(content):
96
- line_no = content[:match.start()].count('\n') + 1
97
- start_line = max(0, line_no - 2)
98
- end_line = min(len(lines), line_no + 2)
99
- snippet = '\n'.join(lines[start_line:end_line])
100
-
101
- findings.append(Finding(
102
- engine=self.name,
103
- severity=rule["severity"],
104
- title=f"{rule['id']}: {rule['title']}",
105
- description=rule["description"],
106
- location=f"{path}:{line_no}",
107
- metadata={
108
- "rule_id": rule["id"],
109
- "cwe": rule["cwe"],
110
- "confidence": rule["confidence"],
111
- "snippet": snippet,
112
- "file": path,
113
- "line": line_no,
114
- }
115
- ))
116
-
117
- return EngineResult(
118
- engine=self.name,
119
- findings=findings,
120
- ok=True,
121
- metrics={"findings_count": len(findings)}
122
- )
123
-
124
-
125
- Engine = LlmSecurityEngine
@@ -1,164 +0,0 @@
1
- """Logging issues detection engine."""
2
- import ast
3
- import re
4
- from orion.engine.config import EngineConfig
5
- from orion.engine.ir import OrionIR
6
- from orion.engine.results import EngineResult, Finding
7
-
8
-
9
- class Engine:
10
- name = "logging_c"
11
- version = "1.0.0"
12
-
13
- PII_PATTERNS = [
14
- "password", "passwd", "pwd", "secret", "token", "api_key", "apikey",
15
- "access_token", "refresh_token", "private_key", "secret_key",
16
- "credit_card", "creditcard", "cc_number", "cvv", "ssn",
17
- "social_security", "email", "mail", "phone", "mobile"
18
- ]
19
-
20
- async def check_availability(self) -> bool:
21
- return True
22
-
23
- def resource_limits(self):
24
- from orion.engine.config import ResourceLimits
25
- return ResourceLimits(max_cpu_seconds=60, max_memory_mb=256)
26
-
27
- async def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
28
- try:
29
- text = ir.content.decode("utf-8", errors="replace")
30
- except Exception:
31
- return EngineResult(engine=self.name, findings=[], ok=True)
32
-
33
- path = ir.metadata.get("path", "unknown")
34
- is_test = self._is_test_file(path)
35
- is_main = self._is_main_file(text)
36
-
37
- if is_test and not self._is_test_logging_utility(text):
38
- return EngineResult(engine=self.name, findings=[], ok=True)
39
-
40
- try:
41
- tree = ast.parse(text, filename=path)
42
- except SyntaxError:
43
- return EngineResult(engine=self.name, findings=[], ok=True)
44
-
45
- findings = []
46
- visitor = LoggingVisitor(path, is_test, is_main, text.splitlines(), self.PII_PATTERNS)
47
- visitor.visit(tree)
48
- findings.extend(visitor.findings)
49
-
50
- if not is_test:
51
- findings.extend(self._check_print_statements(text, path))
52
-
53
- return EngineResult(engine=self.name, findings=findings, ok=True)
54
-
55
- def _is_test_file(self, path: str) -> bool:
56
- test_patterns = ["test_", "_test.py", "/tests/", "/test/", "conftest.py", "pytest", "unittest"]
57
- return any(p in path for p in test_patterns)
58
-
59
- def _is_main_file(self, text: str) -> bool:
60
- return 'if __name__ == "__main__"' in text or "if __name__ == '__main__'" in text
61
-
62
- def _is_test_logging_utility(self, text: str) -> bool:
63
- return "logging" in text and ("test" in text.lower() or "fixture" in text.lower())
64
-
65
- def _check_print_statements(self, text: str, path: str) -> list[Finding]:
66
- findings = []
67
- lines = text.splitlines()
68
- for i, line in enumerate(lines):
69
- if re.search(r"\bprint\s*\(", line) and not re.search(r"#.*print\s*\(", line):
70
- findings.append(Finding(
71
- engine=self.name,
72
- severity="low",
73
- title="Print statement in non-main/non-test file",
74
- description="Use logging module instead of print for production code",
75
- location=f"{path}:{i + 1}",
76
- metadata={
77
- "confidence": 0.8,
78
- "snippet": line.strip()[:200],
79
- "cwe": "CWE-532",
80
- },
81
- ))
82
- return findings
83
-
84
-
85
- class LoggingVisitor(ast.NodeVisitor):
86
- def __init__(self, filepath: str, is_test: bool, is_main: bool, lines: list[str], pii_patterns: list[str]):
87
- self.filepath = filepath
88
- self.is_test = is_test
89
- self.is_main = is_main
90
- self.lines = lines
91
- self.pii_patterns = [p.lower() for p in pii_patterns]
92
- self.findings = []
93
- self.has_getlogger = False
94
- self.basicconfig_count = 0
95
- self.log_level_debug = False
96
- self.in_production = not (is_test or is_main)
97
-
98
- def visit_Call(self, node: ast.Call):
99
- if isinstance(node.func, ast.Attribute):
100
- if node.func.attr == "getLogger" and isinstance(node.func.value, ast.Name) and node.func.value.id == "logging":
101
- self.has_getlogger = True
102
- if node.args:
103
- arg = node.args[0]
104
- if isinstance(arg, ast.Constant) and arg.value == "__name__":
105
- pass
106
- elif isinstance(arg, ast.Name) and arg.id == "__name__":
107
- pass
108
- else:
109
- self._add_finding(node, "Logger name not __name__", "Use logging.getLogger(__name__) for proper hierarchy", 0.75)
110
- elif node.func.attr == "basicConfig" and isinstance(node.func.value, ast.Name) and node.func.value.id == "logging":
111
- self.basicconfig_count += 1
112
- if self.basicconfig_count > 1:
113
- self._add_finding(node, "Multiple logging.basicConfig calls", "basicConfig should be called once at application startup", 0.8)
114
- for kw in node.keywords:
115
- if kw.arg == "level" and isinstance(kw.value, ast.Constant) and kw.value.value == "DEBUG":
116
- if self.in_production:
117
- self._add_finding(node, "Debug log level in production config", "DEBUG level should not be used in production", 0.85)
118
-
119
- if isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name):
120
- if node.func.value.id == "logging" and node.func.attr in ("info", "warning", "error", "critical", "debug", "exception", "log"):
121
- self._check_log_call(node)
122
-
123
- self.generic_visit(node)
124
-
125
- def _check_log_call(self, node: ast.Call):
126
- for arg in node.args:
127
- if isinstance(arg, ast.JoinedStr):
128
- self._check_fstring_for_pii(arg, node)
129
- elif isinstance(arg, ast.Call) and isinstance(arg.func, ast.Name) and arg.func.id == "format":
130
- for farg in arg.args:
131
- if isinstance(farg, ast.JoinedStr):
132
- self._check_fstring_for_pii(farg, node)
133
- elif isinstance(arg, ast.BinOp) and isinstance(arg.op, ast.Mod):
134
- pass
135
-
136
- has_extra = any(kw.arg == "extra" for kw in node.keywords)
137
- has_dict_arg = any(isinstance(kw.value, ast.Dict) for kw in node.keywords)
138
- if not has_extra and not has_dict_arg and self.in_production:
139
- self._add_finding(node, "Structured logging missing (no extra= or dict args)", "Consider using structured logging with extra={} for better observability", 0.75)
140
-
141
- def _check_fstring_for_pii(self, fstring: ast.JoinedStr, call_node: ast.Call):
142
- for part in fstring.values:
143
- if isinstance(part, ast.FormattedValue):
144
- if isinstance(part.value, ast.Name):
145
- var_name = part.value.id.lower()
146
- for pii in self.pii_patterns:
147
- if pii in var_name:
148
- self._add_finding(call_node, f"PII in log f-string: {pii}", f"Logging potential PII ({pii}) in f-string violates privacy", 0.85, "CWE-532")
149
-
150
- def _add_finding(self, node: ast.AST, title: str, description: str, confidence: float, cwe: str = "CWE-532"):
151
- line_no = getattr(node, "lineno", 0)
152
- snippet = self.lines[line_no - 1].strip() if 0 < line_no <= len(self.lines) else ""
153
- self.findings.append(Finding(
154
- engine="logging_c",
155
- severity="high" if confidence >= 0.85 else "medium",
156
- title=title,
157
- description=description,
158
- location=f"{self.filepath}:{line_no}",
159
- metadata={
160
- "confidence": confidence,
161
- "snippet": snippet[:200],
162
- "cwe": cwe,
163
- },
164
- ))
@@ -1,140 +0,0 @@
1
- """Finding noise suppression engine - filters false positives from prior findings."""
2
- import subprocess
3
- import os
4
- from pathlib import Path
5
- from orion.engine.config import EngineConfig
6
- from orion.engine.ir import OrionIR
7
- from orion.engine.results import EngineResult, Finding
8
-
9
-
10
- class Engine:
11
- name = "noise"
12
- version = "1.0.0"
13
-
14
- GENERATED_FILE_PATTERNS = [
15
- "_pb2.py", "_grpc.py", "_pb2_grpc.py",
16
- "migrations/", "alembic/versions/",
17
- "vendor/", "node_modules/", ".venv/", "venv/",
18
- "dist/", "build/", "__pycache__/", ".pytest_cache/",
19
- "generated/", "auto_generated/", "thrift_gen/",
20
- ".proto.py", "_proto.py",
21
- ]
22
-
23
- HIGH_CHURN_THRESHOLD = 5
24
- RECENT_DAYS = 30
25
- LOW_CONFIDENCE_THRESHOLD = 0.6
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
- async def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
35
- prior_findings = self._extract_prior_findings(ir, config)
36
- if not prior_findings:
37
- return EngineResult(engine=self.name, findings=[], ok=True, metrics={"filtered": 0, "total": 0})
38
-
39
- path = ir.metadata.get("path", "unknown")
40
- project_root = ir.metadata.get("project_root", ".")
41
-
42
- filtered = []
43
- noise_count = 0
44
-
45
- for finding in prior_findings:
46
- is_noise = False
47
- reasons = []
48
-
49
- if finding.metadata.get("confidence", 1.0) < self.LOW_CONFIDENCE_THRESHOLD:
50
- if self._is_recently_modified(finding.location, project_root):
51
- if self._has_high_churn(finding.location, project_root):
52
- is_noise = True
53
- reasons.append("low_confidence + recent_modification + high_churn")
54
-
55
- if self._is_duplicate_finding(finding, prior_findings):
56
- is_noise = True
57
- reasons.append("duplicate_across_runs")
58
-
59
- if self._is_generated_file(finding.location):
60
- is_noise = True
61
- reasons.append("generated_file")
62
-
63
- if is_noise:
64
- noise_count += 1
65
- finding.metadata["noise_reasons"] = reasons
66
- finding.metadata["suppressed_by"] = "noise_engine"
67
- else:
68
- filtered.append(finding)
69
-
70
- return EngineResult(
71
- engine=self.name,
72
- findings=filtered,
73
- ok=True,
74
- metrics={
75
- "filtered": noise_count,
76
- "total": len(prior_findings),
77
- "remaining": len(filtered),
78
- },
79
- )
80
-
81
- def _extract_prior_findings(self, ir: OrionIR, config: EngineConfig) -> list[Finding]:
82
- if hasattr(ir, "prior_findings") and ir.prior_findings:
83
- return ir.prior_findings
84
-
85
- if config.toggles and "prior_findings" in config.toggles:
86
- return config.toggles["prior_findings"]
87
-
88
- if ir.metadata.get("prior_findings"):
89
- return ir.metadata["prior_findings"]
90
-
91
- return []
92
-
93
- def _is_recently_modified(self, location: str, project_root: str) -> bool:
94
- try:
95
- filepath = location.split(":")[0] if ":" in location else location
96
- full_path = Path(project_root) / filepath
97
- if not full_path.exists():
98
- return False
99
-
100
- result = subprocess.run(
101
- ["git", "log", "--since", f"{self.RECENT_DAYS} days ago", "--oneline", str(full_path)],
102
- capture_output=True, text=True, cwd=project_root, timeout=10
103
- )
104
- return result.returncode == 0 and len(result.stdout.strip().splitlines()) > 0
105
- except Exception:
106
- return False
107
-
108
- def _has_high_churn(self, location: str, project_root: str) -> bool:
109
- try:
110
- filepath = location.split(":")[0] if ":" in location else location
111
- full_path = Path(project_root) / filepath
112
- if not full_path.exists():
113
- return False
114
-
115
- result = subprocess.run(
116
- ["git", "log", "--oneline", str(full_path)],
117
- capture_output=True, text=True, cwd=project_root, timeout=10
118
- )
119
- if result.returncode == 0:
120
- commit_count = len(result.stdout.strip().splitlines())
121
- return commit_count > self.HIGH_CHURN_THRESHOLD
122
- except Exception:
123
- pass
124
- return False
125
-
126
- def _is_duplicate_finding(self, finding: Finding, all_findings: list[Finding]) -> bool:
127
- count = 0
128
- for other in all_findings:
129
- if (other.engine == finding.engine and
130
- other.title == finding.title and
131
- other.location == finding.location):
132
- count += 1
133
- return count > 1
134
-
135
- def _is_generated_file(self, location: str) -> bool:
136
- filepath = location.split(":")[0] if ":" in location else location
137
- for pattern in self.GENERATED_FILE_PATTERNS:
138
- if pattern in filepath:
139
- return True
140
- return False
@@ -1,244 +0,0 @@
1
- """Orphan file detection engine - finds files with zero incoming imports."""
2
- import ast
3
- import re
4
- from pathlib import Path
5
- from typing import Dict, List, Set, Optional
6
- from collections import defaultdict
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 OrphanEngine:
14
- name = "orphan"
15
- version = "1.0.0"
16
-
17
- EXCLUDED_PATTERNS = {
18
- "__init__.py",
19
- "__main__.py",
20
- "conftest.py",
21
- "manage.py",
22
- "wsgi.py",
23
- "asgi.py",
24
- "celery.py",
25
- "gunicorn.conf.py",
26
- "pytest.ini",
27
- }
28
-
29
- EXCLUDED_DIRS = {
30
- "migrations",
31
- "tests",
32
- "test",
33
- "scripts",
34
- "__pycache__",
35
- ".git",
36
- ".venv",
37
- "venv",
38
- "env",
39
- "node_modules",
40
- "dist",
41
- "build",
42
- }
43
-
44
- async def check_availability(self) -> bool:
45
- return True
46
-
47
- def resource_limits(self):
48
- from orion.engine.config import ResourceLimits
49
- return ResourceLimits(max_cpu_seconds=60, max_memory_mb=256)
50
-
51
- def _should_exclude(self, filepath: str, config: EngineConfig) -> bool:
52
- path = Path(filepath)
53
- # Check excluded filenames
54
- if path.name in self.EXCLUDED_PATTERNS:
55
- return True
56
- # Check excluded directories
57
- for part in path.parts:
58
- if part in self.EXCLUDED_DIRS:
59
- return True
60
- # Check config exclude patterns
61
- if config.toggles.get("exclude"):
62
- for pattern in config.toggles["exclude"]:
63
- if path.match(pattern) or any(p.match(pattern) for p in path.parents):
64
- return True
65
- return False
66
-
67
- def _is_python_file(self, filepath: str) -> bool:
68
- return Path(filepath).suffix == ".py"
69
-
70
- def _extract_imports(self, content: str, filepath: str) -> List[str]:
71
- """Extract all imported module names from a Python file."""
72
- imports = []
73
- try:
74
- tree = ast.parse(content)
75
- for node in ast.walk(tree):
76
- if isinstance(node, ast.Import):
77
- for alias in node.names:
78
- imports.append(alias.name) # Keep full module path
79
- elif isinstance(node, ast.ImportFrom):
80
- if node.module:
81
- imports.append(node.module) # Keep full module path
82
- except SyntaxError:
83
- # Fallback to regex for files with syntax errors
84
- import_pattern = re.compile(r'^\s*(?:from\s+(\S+)\s+import|import\s+(\S+))', re.MULTILINE)
85
- for match in import_pattern.finditer(content):
86
- mod = match.group(1) or match.group(2)
87
- if mod:
88
- imports.append(mod)
89
- return imports
90
-
91
- def _get_module_name(self, filepath: str, project_root: str) -> Optional[str]:
92
- """Convert filepath to module name relative to project root."""
93
- try:
94
- path = Path(filepath).relative_to(Path(project_root))
95
- except ValueError:
96
- # File is outside project root
97
- return Path(filepath).stem
98
-
99
- # Remove .py extension
100
- if path.suffix == ".py":
101
- path = path.with_suffix("")
102
-
103
- # Convert path to module notation
104
- parts = list(path.parts)
105
- if parts and parts[-1] == "__init__":
106
- parts = parts[:-1]
107
-
108
- return ".".join(parts) if parts else path.stem
109
-
110
- def _resolve_import(self, import_name: str, importer_module: str, project_root: str, module_to_file: Dict[str, str]) -> Optional[str]:
111
- """Resolve an import name to a module in the project."""
112
- # Try exact match first
113
- if import_name in module_to_file:
114
- return import_name
115
-
116
- # Try relative to importer's package (for relative imports)
117
- if import_name.startswith('.'):
118
- importer_parts = importer_module.split('.') if importer_module else []
119
- level = len(import_name) - len(import_name.lstrip('.'))
120
- base_parts = importer_parts[:-level] if level <= len(importer_parts) else []
121
- relative_name = import_name.lstrip('.')
122
- if relative_name:
123
- candidate = '.'.join(base_parts + [relative_name])
124
- else:
125
- candidate = '.'.join(base_parts)
126
- if candidate in module_to_file:
127
- return candidate
128
-
129
- # Try as absolute import
130
- if import_name in module_to_file:
131
- return import_name
132
-
133
- # Try with importer's package prefix
134
- importer_parts = importer_module.split('.') if importer_module else []
135
- for i in range(len(importer_parts), -1, -1):
136
- prefix = '.'.join(importer_parts[:i])
137
- candidate = f"{prefix}.{import_name}" if prefix else import_name
138
- if candidate in module_to_file:
139
- return candidate
140
-
141
- return None
142
-
143
- async def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
144
- # This engine needs project-wide context, so we need to access all files
145
- # The IR metadata should contain project information
146
- project_root = ir.metadata.get("project_root", ".")
147
- all_files = ir.metadata.get("all_files", {})
148
-
149
- if not all_files:
150
- # Single file mode - can't do orphan detection
151
- return EngineResult(engine=self.name, ok=True, metrics={"mode": "single_file"})
152
-
153
- # Build import graph
154
- module_to_file = {}
155
- file_to_module = {}
156
- imports_map = defaultdict(set) # module -> set of imported modules
157
- reverse_deps = defaultdict(set) # module -> set of modules that import it
158
-
159
- # First pass: map files to modules
160
- for filepath, file_ir in all_files.items():
161
- if not self._is_python_file(filepath):
162
- continue
163
- if self._should_exclude(filepath, config):
164
- continue
165
-
166
- module_name = self._get_module_name(filepath, project_root)
167
- if module_name:
168
- module_to_file[module_name] = filepath
169
- file_to_module[filepath] = module_name
170
-
171
- # Second pass: extract imports
172
- for filepath, file_ir in all_files.items():
173
- if not self._is_python_file(filepath):
174
- continue
175
- if self._should_exclude(filepath, config):
176
- continue
177
-
178
- module_name = file_to_module.get(filepath)
179
- if not module_name:
180
- continue
181
-
182
- try:
183
- content = file_ir.content.decode("utf-8", errors="replace")
184
- except Exception:
185
- continue
186
-
187
- imports = self._extract_imports(content, filepath)
188
- for imp in imports:
189
- resolved = self._resolve_import(imp, module_name, project_root, module_to_file)
190
- if resolved:
191
- imports_map[module_name].add(resolved)
192
- reverse_deps[resolved].add(module_name)
193
-
194
- # Find orphans: modules with zero incoming imports
195
- orphans = []
196
- for module_name, filepath in module_to_file.items():
197
- if module_name not in reverse_deps or not reverse_deps[module_name]:
198
- # Check if it's a legitimate entry point
199
- path = Path(filepath)
200
- is_entry_point = any(
201
- pattern in str(path) for pattern in [
202
- "main.py", "app.py", "cli.py", "run.py", "start.py",
203
- "entrypoint.py", "bootstrap.py", "__main__.py",
204
- ]
205
- )
206
- is_test = "test" in str(path).lower()
207
- is_script = "script" in str(path).lower()
208
-
209
- if not is_entry_point and not is_test and not is_script:
210
- orphans.append({
211
- "module": module_name,
212
- "file": filepath,
213
- "reason": "no_incoming_imports",
214
- })
215
-
216
- # Create findings
217
- findings = []
218
- for orphan in orphans:
219
- findings.append(Finding(
220
- engine=self.name,
221
- severity="medium",
222
- title=f"Orphan file: {orphan['file']}",
223
- description=f"File has no incoming imports - may be dead code",
224
- location=orphan["file"],
225
- metadata={
226
- "file": orphan["file"],
227
- "module": orphan["module"],
228
- "reason": orphan["reason"],
229
- },
230
- ))
231
-
232
- return EngineResult(
233
- engine=self.name,
234
- findings=findings,
235
- ok=True,
236
- metrics={
237
- "total_modules": len(module_to_file),
238
- "orphans_found": len(orphans),
239
- "import_edges": sum(len(v) for v in imports_map.values()),
240
- },
241
- )
242
-
243
-
244
- Engine = OrphanEngine