@orionscanner/cli 0.1.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -190
- package/bin/orion.js +30 -29
- package/dist/orion.exe +0 -0
- package/package.json +46 -48
- package/docs/API_REFERENCE.md +0 -211
- package/docs/ARCHITECTURE.md +0 -183
- package/docs/CHANGELOG.md +0 -112
- package/docs/CONTRIBUTING.md +0 -157
- package/docs/ENGINE_CATALOG.md +0 -87
- package/docs/GETTING_STARTED.md +0 -226
- package/pyproject.toml +0 -16
- package/src/orion/__init__.py +0 -0
- package/src/orion/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/orion/engine/__init__.py +0 -17
- package/src/orion/engine/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/orion/engine/__pycache__/config.cpython-313.pyc +0 -0
- package/src/orion/engine/__pycache__/core.cpython-313.pyc +0 -0
- package/src/orion/engine/__pycache__/ir.cpython-313.pyc +0 -0
- package/src/orion/engine/__pycache__/languages.cpython-313.pyc +0 -0
- package/src/orion/engine/__pycache__/registry.cpython-313.pyc +0 -0
- package/src/orion/engine/__pycache__/results.cpython-313.pyc +0 -0
- package/src/orion/engine/__pycache__/runner.cpython-313.pyc +0 -0
- package/src/orion/engine/api_security/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/api_security/engine.py +0 -125
- package/src/orion/engine/architecture/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/architecture/engine.py +0 -463
- package/src/orion/engine/authorization/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/authorization/engine.py +0 -337
- package/src/orion/engine/callgraph/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/callgraph/engine.py +0 -236
- package/src/orion/engine/cfg/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/cfg/engine.py +0 -509
- package/src/orion/engine/chaos/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/chaos/engine.py +0 -268
- package/src/orion/engine/chaos_config/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/chaos_config/engine.py +0 -322
- package/src/orion/engine/complexity/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/complexity/engine.py +0 -144
- package/src/orion/engine/compliance/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/compliance/engine.py +0 -325
- package/src/orion/engine/config.py +0 -41
- package/src/orion/engine/config_engine/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/config_engine/engine.py +0 -364
- package/src/orion/engine/config_validation/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/config_validation/engine.py +0 -313
- package/src/orion/engine/container/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/container/engine.py +0 -546
- package/src/orion/engine/contract_probe/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/contract_probe/engine.py +0 -316
- package/src/orion/engine/core.py +0 -75
- package/src/orion/engine/crypto_guard/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/crypto_guard/engine.py +0 -134
- package/src/orion/engine/data_residency/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/data_residency/engine.py +0 -238
- package/src/orion/engine/dataflow/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/dataflow/engine.py +0 -398
- package/src/orion/engine/dead_import/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/dead_import/engine.py +0 -293
- package/src/orion/engine/deps/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/deps/engine.py +0 -244
- package/src/orion/engine/dispatcher.py +0 -350
- package/src/orion/engine/docs/AUDIT_PROMPT.md +0 -1248
- package/src/orion/engine/duplication/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/duplication/engine.py +0 -120
- package/src/orion/engine/error_handling/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/error_handling/engine.py +0 -256
- package/src/orion/engine/fp_learn/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/fp_learn/engine.py +0 -248
- package/src/orion/engine/hunter/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/hunter/engine.py +0 -324
- package/src/orion/engine/iac_security/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/iac_security/engine.py +0 -218
- package/src/orion/engine/ir.py +0 -22
- package/src/orion/engine/languages.py +0 -454
- package/src/orion/engine/license/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/license/engine.py +0 -172
- package/src/orion/engine/lint/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/lint/engine.py +0 -558
- package/src/orion/engine/llm_security/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/llm_security/engine.py +0 -125
- package/src/orion/engine/logging_c/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/logging_c/engine.py +0 -164
- package/src/orion/engine/noise/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/noise/engine.py +0 -140
- package/src/orion/engine/orphan/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/orphan/engine.py +0 -244
- package/src/orion/engine/perf_guard/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/perf_guard/engine.py +0 -345
- package/src/orion/engine/prompt_security/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/prompt_security/detectors/__init__.py +0 -4
- package/src/orion/engine/prompt_security/engine.py +0 -170
- package/src/orion/engine/provenance/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/provenance/engine.py +0 -242
- package/src/orion/engine/registry.py +0 -55
- package/src/orion/engine/regression/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/regression/engine.py +0 -213
- package/src/orion/engine/results.py +0 -21
- package/src/orion/engine/runner.py +0 -1310
- package/src/orion/engine/sandbox_escape/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/sandbox_escape/engine.py +0 -284
- package/src/orion/engine/sast/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/sast/engine.py +0 -217
- package/src/orion/engine/sast/patterns/__init__.py +0 -513
- package/src/orion/engine/sast/patterns/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/orion/engine/secrets/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/secrets/engine.py +0 -77
- package/src/orion/engine/secrets/patterns/__init__.py +0 -74
- package/src/orion/engine/secrets/patterns/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/orion/engine/sensitive_data/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/sensitive_data/engine.py +0 -143
- package/src/orion/engine/state/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/state/engine.py +0 -218
- package/src/orion/engine/supply_chain_sbom/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/supply_chain_sbom/engine.py +0 -290
- package/src/orion/engine/symbol/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/symbol/engine.py +0 -463
- package/src/orion/engine/taint/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/taint/engine.py +0 -464
- package/src/orion/engine/test_coverage/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/test_coverage/engine.py +0 -445
- package/src/orion/engine/threat_model/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/threat_model/engine.py +0 -126
- package/src/orion/engine/wiring_trace/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/wiring_trace/engine.py +0 -200
- package/src/orion_engine.egg-info/PKG-INFO +0 -7
- package/src/orion_engine.egg-info/SOURCES.txt +0 -65
- package/src/orion_engine.egg-info/dependency_links.txt +0 -1
- package/src/orion_engine.egg-info/entry_points.txt +0 -2
- package/src/orion_engine.egg-info/top_level.txt +0 -1
|
@@ -1,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
|
|
Binary file
|
|
@@ -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
|
-
))
|
|
Binary file
|
|
@@ -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
|
|
Binary file
|
|
@@ -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
|
|
Binary file
|