@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,238 +0,0 @@
|
|
|
1
|
-
"""PII detection engine - regex-based detection of hardcoded PII in code."""
|
|
2
|
-
import re
|
|
3
|
-
import hashlib
|
|
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 = "data_residency"
|
|
11
|
-
version = "1.0.0"
|
|
12
|
-
|
|
13
|
-
PII_PATTERNS = {
|
|
14
|
-
"email": re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"),
|
|
15
|
-
"phone": re.compile(r"\+?\d{1,3}[-.\s]?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}"),
|
|
16
|
-
"ssn": re.compile(r"\b\d{3}-\d{2}-\d{4}\b"),
|
|
17
|
-
"credit_card": re.compile(r"\b(?:\d{4}[-\s]?){3}\d{4}\b"),
|
|
18
|
-
"ipv4": re.compile(r"\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b"),
|
|
19
|
-
"uuid": re.compile(r"\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b", re.IGNORECASE),
|
|
20
|
-
"api_key": re.compile(r"(?i)(?:api[_-]?key|access[_-]?token|secret[_-]?key)['\"]?\s*[:=]\s*['\"]?([a-zA-Z0-9_\-]{20,})"),
|
|
21
|
-
"jwt": re.compile(r"eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+"),
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
EXCLUDE_PATTERNS = [
|
|
25
|
-
r"test_", r"_test\.py$", r"/tests/", r"/test/", r"conftest\.py",
|
|
26
|
-
r"\.example", r"\.sample", r"\.template", r"fixture", r"mock",
|
|
27
|
-
r"example\.com", r"test@.*", r"user@.*", r"admin@.*",
|
|
28
|
-
r"127\.0\.0\.1", r"localhost", r"0\.0\.0\.0", r"::1",
|
|
29
|
-
r"555[-.\s]?\d{3}[-.\s]?\d{4}", # fake phone prefix
|
|
30
|
-
r"123-45-6789", # fake SSN
|
|
31
|
-
r"4111[-.\s]?1111[-.\s]?1111[-.\s]?1111", # test CC
|
|
32
|
-
]
|
|
33
|
-
|
|
34
|
-
def __init__(self):
|
|
35
|
-
self._exclude_regex = [re.compile(p, re.IGNORECASE) for p in self.EXCLUDE_PATTERNS]
|
|
36
|
-
|
|
37
|
-
async def check_availability(self) -> bool:
|
|
38
|
-
return True
|
|
39
|
-
|
|
40
|
-
def resource_limits(self):
|
|
41
|
-
from orion.engine.config import ResourceLimits
|
|
42
|
-
return ResourceLimits(max_cpu_seconds=60, max_memory_mb=256)
|
|
43
|
-
|
|
44
|
-
async def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
|
|
45
|
-
try:
|
|
46
|
-
text = ir.content.decode("utf-8", errors="replace")
|
|
47
|
-
except Exception:
|
|
48
|
-
return EngineResult(engine=self.name, findings=[], ok=True)
|
|
49
|
-
|
|
50
|
-
path = ir.metadata.get("path", "unknown")
|
|
51
|
-
|
|
52
|
-
if self._should_exclude(path, text):
|
|
53
|
-
return EngineResult(engine=self.name, findings=[], ok=True)
|
|
54
|
-
|
|
55
|
-
findings = []
|
|
56
|
-
lines = text.splitlines()
|
|
57
|
-
|
|
58
|
-
findings.extend(self._scan_regex_patterns(text, lines, path))
|
|
59
|
-
findings.extend(self._scan_luhn_valid_cards(text, lines, path))
|
|
60
|
-
|
|
61
|
-
return EngineResult(engine=self.name, findings=findings, ok=True)
|
|
62
|
-
|
|
63
|
-
def _should_exclude(self, path: str, text: str) -> bool:
|
|
64
|
-
for pattern in self._exclude_regex:
|
|
65
|
-
if pattern.search(path):
|
|
66
|
-
return True
|
|
67
|
-
return False
|
|
68
|
-
|
|
69
|
-
def _scan_regex_patterns(self, text: str, lines: list[str], path: str) -> list[Finding]:
|
|
70
|
-
findings = []
|
|
71
|
-
for pii_type, pattern in self.PII_PATTERNS.items():
|
|
72
|
-
for match in pattern.finditer(text):
|
|
73
|
-
if pii_type == "credit_card" and not self._luhn_check(match.group()):
|
|
74
|
-
continue
|
|
75
|
-
|
|
76
|
-
line_no = text[:match.start()].count("\n") + 1
|
|
77
|
-
context_start = max(0, match.start() - 50)
|
|
78
|
-
context_end = min(len(text), match.end() + 50)
|
|
79
|
-
context = text[context_start:context_end]
|
|
80
|
-
|
|
81
|
-
if self._is_in_comment_or_string(text, match.start(), lines, line_no):
|
|
82
|
-
continue
|
|
83
|
-
|
|
84
|
-
if self._is_false_positive(pii_type, match.group(), context):
|
|
85
|
-
continue
|
|
86
|
-
|
|
87
|
-
confidence = self._calculate_confidence(pii_type, match.group(), context)
|
|
88
|
-
if confidence >= 0.7:
|
|
89
|
-
findings.append(Finding(
|
|
90
|
-
engine=self.name,
|
|
91
|
-
severity="high" if pii_type in ("ssn", "credit_card", "api_key", "jwt") else "medium",
|
|
92
|
-
title=f"PII detected: {pii_type}",
|
|
93
|
-
description=f"Hardcoded {pii_type} found in source code",
|
|
94
|
-
location=f"{path}:{line_no}",
|
|
95
|
-
metadata={
|
|
96
|
-
"confidence": confidence,
|
|
97
|
-
"pii_type": pii_type,
|
|
98
|
-
"snippet": self._mask_pii(match.group(), pii_type),
|
|
99
|
-
"context": context[:200],
|
|
100
|
-
"cwe": "CWE-532",
|
|
101
|
-
},
|
|
102
|
-
))
|
|
103
|
-
|
|
104
|
-
return findings
|
|
105
|
-
|
|
106
|
-
def _scan_luhn_valid_cards(self, text: str, lines: list[str], path: str) -> list[Finding]:
|
|
107
|
-
findings = []
|
|
108
|
-
cc_pattern = re.compile(r"\b(?:\d{4}[-\s]?){3}\d{4}\b|\b\d{13,19}\b")
|
|
109
|
-
for match in cc_pattern.finditer(text):
|
|
110
|
-
card_num = match.group().replace(" ", "").replace("-", "")
|
|
111
|
-
if len(card_num) >= 13 and len(card_num) <= 19 and card_num.isdigit():
|
|
112
|
-
if self._luhn_check(card_num):
|
|
113
|
-
line_no = text[:match.start()].count("\n") + 1
|
|
114
|
-
if not self._is_in_comment_or_string(text, match.start(), lines, line_no):
|
|
115
|
-
findings.append(Finding(
|
|
116
|
-
engine=self.name,
|
|
117
|
-
severity="high",
|
|
118
|
-
title="PII detected: credit_card (Luhn valid)",
|
|
119
|
-
description="Hardcoded credit card number (passes Luhn check) found in source code",
|
|
120
|
-
location=f"{path}:{line_no}",
|
|
121
|
-
metadata={
|
|
122
|
-
"confidence": 0.9,
|
|
123
|
-
"pii_type": "credit_card",
|
|
124
|
-
"snippet": self._mask_pii(match.group(), "credit_card"),
|
|
125
|
-
"cwe": "CWE-532",
|
|
126
|
-
},
|
|
127
|
-
))
|
|
128
|
-
return findings
|
|
129
|
-
|
|
130
|
-
def _is_in_comment_or_string(self, text: str, pos: int, lines: list[str], line_no: int) -> bool:
|
|
131
|
-
line_idx = line_no - 1
|
|
132
|
-
if 0 <= line_idx < len(lines):
|
|
133
|
-
line = lines[line_idx]
|
|
134
|
-
col = pos - (len(text) - len("\n".join(lines[line_idx:])))
|
|
135
|
-
if col < 0:
|
|
136
|
-
col = 0
|
|
137
|
-
|
|
138
|
-
in_string = False
|
|
139
|
-
string_char = None
|
|
140
|
-
escaped = False
|
|
141
|
-
for i, ch in enumerate(line[:col]):
|
|
142
|
-
if escaped:
|
|
143
|
-
escaped = False
|
|
144
|
-
continue
|
|
145
|
-
if ch == "\\":
|
|
146
|
-
escaped = True
|
|
147
|
-
continue
|
|
148
|
-
if not in_string and ch in ('"', "'", "`"):
|
|
149
|
-
in_string = True
|
|
150
|
-
string_char = ch
|
|
151
|
-
elif in_string and ch == string_char:
|
|
152
|
-
in_string = False
|
|
153
|
-
string_char = None
|
|
154
|
-
if in_string:
|
|
155
|
-
return True
|
|
156
|
-
|
|
157
|
-
comment_pos = line.find("#")
|
|
158
|
-
if comment_pos >= 0 and col > comment_pos:
|
|
159
|
-
return True
|
|
160
|
-
|
|
161
|
-
return False
|
|
162
|
-
|
|
163
|
-
def _is_false_positive(self, pii_type: str, value: str, context: str) -> bool:
|
|
164
|
-
context_lower = context.lower()
|
|
165
|
-
|
|
166
|
-
if pii_type == "email":
|
|
167
|
-
if any(fake in value.lower() for fake in ["example", "test", "dummy", "fake", "placeholder", "user@", "admin@", "noreply@", "no-reply@"]):
|
|
168
|
-
return True
|
|
169
|
-
if any(fake in context_lower for fake in ["example.com", "test.com", "localhost", "invalid", "placeholder"]):
|
|
170
|
-
return True
|
|
171
|
-
|
|
172
|
-
if pii_type == "phone":
|
|
173
|
-
if value.startswith(("555", "123", "000")):
|
|
174
|
-
return True
|
|
175
|
-
|
|
176
|
-
if pii_type == "ssn":
|
|
177
|
-
if value in ("123-45-6789", "000-00-0000", "999-99-9999"):
|
|
178
|
-
return True
|
|
179
|
-
|
|
180
|
-
if pii_type == "ipv4":
|
|
181
|
-
if value.startswith(("127.", "10.", "192.168.", "172.16.", "172.17.", "172.18.", "172.19.", "172.20.", "172.21.", "172.22.", "172.23.", "172.24.", "172.25.", "172.26.", "172.27.", "172.28.", "172.29.", "172.30.", "172.31.", "0.0.0.0", "255.255.255.255")):
|
|
182
|
-
return True
|
|
183
|
-
|
|
184
|
-
if pii_type == "uuid":
|
|
185
|
-
if value in ("00000000-0000-0000-0000-000000000000", "ffffffff-ffff-ffff-ffff-ffffffffffff"):
|
|
186
|
-
return True
|
|
187
|
-
|
|
188
|
-
return False
|
|
189
|
-
|
|
190
|
-
def _calculate_confidence(self, pii_type: str, value: str, context: str) -> float:
|
|
191
|
-
base_confidence = {
|
|
192
|
-
"email": 0.75,
|
|
193
|
-
"phone": 0.7,
|
|
194
|
-
"ssn": 0.9,
|
|
195
|
-
"credit_card": 0.85,
|
|
196
|
-
"ipv4": 0.65,
|
|
197
|
-
"uuid": 0.8,
|
|
198
|
-
"api_key": 0.85,
|
|
199
|
-
"jwt": 0.95,
|
|
200
|
-
}.get(pii_type, 0.7)
|
|
201
|
-
|
|
202
|
-
context_lower = context.lower()
|
|
203
|
-
if any(kw in context_lower for kw in ["config", "secret", "password", "token", "key", "credential", "auth"]):
|
|
204
|
-
base_confidence += 0.1
|
|
205
|
-
if any(kw in context_lower for kw in ["example", "test", "demo", "sample", "placeholder", "fake"]):
|
|
206
|
-
base_confidence -= 0.2
|
|
207
|
-
|
|
208
|
-
return max(0.5, min(1.0, base_confidence))
|
|
209
|
-
|
|
210
|
-
def _luhn_check(self, card_num: str) -> bool:
|
|
211
|
-
digits = [int(d) for d in card_num if d.isdigit()]
|
|
212
|
-
if len(digits) < 13:
|
|
213
|
-
return False
|
|
214
|
-
checksum = 0
|
|
215
|
-
for i, d in enumerate(reversed(digits)):
|
|
216
|
-
if i % 2 == 1:
|
|
217
|
-
d *= 2
|
|
218
|
-
if d > 9:
|
|
219
|
-
d -= 9
|
|
220
|
-
checksum += d
|
|
221
|
-
return checksum % 10 == 0
|
|
222
|
-
|
|
223
|
-
def _mask_pii(self, value: str, pii_type: str) -> str:
|
|
224
|
-
if pii_type == "email":
|
|
225
|
-
parts = value.split("@")
|
|
226
|
-
if len(parts) == 2:
|
|
227
|
-
return parts[0][:2] + "***" + "@" + parts[1]
|
|
228
|
-
elif pii_type in ("credit_card", "ssn"):
|
|
229
|
-
return value[:4] + "*" * (len(value) - 8) + value[-4:] if len(value) > 8 else "*" * len(value)
|
|
230
|
-
elif pii_type == "phone":
|
|
231
|
-
return value[:3] + "***" + value[-2:] if len(value) > 5 else "*" * len(value)
|
|
232
|
-
elif pii_type == "api_key":
|
|
233
|
-
return value[:8] + "*" * (len(value) - 12) + value[-4:] if len(value) > 12 else "*" * len(value)
|
|
234
|
-
elif pii_type == "jwt":
|
|
235
|
-
parts = value.split(".")
|
|
236
|
-
if len(parts) == 3:
|
|
237
|
-
return parts[0][:10] + "..." + parts[2][-10:]
|
|
238
|
-
return value[:4] + "*" * max(0, len(value) - 8) + value[-4:] if len(value) > 8 else "*" * len(value)
|
|
Binary file
|
|
@@ -1,398 +0,0 @@
|
|
|
1
|
-
"""Variable data flow analyzer for detecting use-before-def, dead stores, mutable defaults, and global mutations."""
|
|
2
|
-
import ast
|
|
3
|
-
from typing import Dict, List, Set, Optional, Any, Tuple
|
|
4
|
-
from dataclasses import dataclass, field
|
|
5
|
-
from collections import defaultdict
|
|
6
|
-
from orion.engine.config import EngineConfig, ResourceLimits
|
|
7
|
-
from orion.engine.ir import OrionIR
|
|
8
|
-
from orion.engine.results import EngineResult, Finding
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@dataclass
|
|
12
|
-
class VarInfo:
|
|
13
|
-
name: str
|
|
14
|
-
defined_at: List[int] = field(default_factory=list)
|
|
15
|
-
used_at: List[int] = field(default_factory=list)
|
|
16
|
-
assigned_at: List[int] = field(default_factory=list)
|
|
17
|
-
is_parameter: bool = False
|
|
18
|
-
is_global: bool = False
|
|
19
|
-
is_nonlocal: bool = False
|
|
20
|
-
is_mutable_default: bool = False
|
|
21
|
-
default_node: Optional[ast.AST] = None
|
|
22
|
-
scope_level: int = 0
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class DataFlowVisitor(ast.NodeVisitor):
|
|
26
|
-
def __init__(self, filename: str, source_lines: List[str]):
|
|
27
|
-
self.filename = filename
|
|
28
|
-
self.source_lines = source_lines
|
|
29
|
-
self.findings: List[Finding] = []
|
|
30
|
-
self.variables: Dict[str, VarInfo] = {}
|
|
31
|
-
self.scope_stack: List[Dict[str, VarInfo]] = [{}]
|
|
32
|
-
self.current_function: Optional[str] = None
|
|
33
|
-
self.global_names: Set[str] = set()
|
|
34
|
-
self.nonlocal_names: Set[str] = set()
|
|
35
|
-
self.loop_stack: List[int] = []
|
|
36
|
-
self.assignment_targets: Dict[int, List[str]] = defaultdict(list)
|
|
37
|
-
|
|
38
|
-
def get_snippet(self, node: ast.AST, context: int = 2) -> str:
|
|
39
|
-
start = max(0, node.lineno - 1 - context)
|
|
40
|
-
end = min(len(self.source_lines),
|
|
41
|
-
getattr(node, 'end_lineno', node.lineno) + context if hasattr(node, 'end_lineno') else node.lineno + context)
|
|
42
|
-
return "\n".join(self.source_lines[start:end])
|
|
43
|
-
|
|
44
|
-
def current_scope(self) -> Dict[str, VarInfo]:
|
|
45
|
-
return self.scope_stack[-1]
|
|
46
|
-
|
|
47
|
-
def define_var(self, name: str, node: ast.AST, is_parameter: bool = False):
|
|
48
|
-
scope = self.current_scope()
|
|
49
|
-
if name not in scope:
|
|
50
|
-
scope[name] = VarInfo(name=name, scope_level=len(self.scope_stack))
|
|
51
|
-
var = scope[name]
|
|
52
|
-
var.defined_at.append(node.lineno)
|
|
53
|
-
var.assigned_at.append(node.lineno)
|
|
54
|
-
if is_parameter:
|
|
55
|
-
var.is_parameter = True
|
|
56
|
-
if name in self.global_names:
|
|
57
|
-
var.is_global = True
|
|
58
|
-
if name in self.nonlocal_names:
|
|
59
|
-
var.is_nonlocal = True
|
|
60
|
-
|
|
61
|
-
def use_var(self, name: str, node: ast.AST):
|
|
62
|
-
scope = self.current_scope()
|
|
63
|
-
found = False
|
|
64
|
-
for s in reversed(self.scope_stack):
|
|
65
|
-
if name in s:
|
|
66
|
-
s[name].used_at.append(node.lineno)
|
|
67
|
-
found = True
|
|
68
|
-
break
|
|
69
|
-
if not found and name not in self.global_names:
|
|
70
|
-
pass
|
|
71
|
-
|
|
72
|
-
def visit_Module(self, node: ast.Module):
|
|
73
|
-
self.generic_visit(node)
|
|
74
|
-
self._check_dead_stores()
|
|
75
|
-
self._check_mutable_defaults()
|
|
76
|
-
self._check_global_mutations()
|
|
77
|
-
|
|
78
|
-
def visit_FunctionDef(self, node: ast.FunctionDef):
|
|
79
|
-
old_function = self.current_function
|
|
80
|
-
self.current_function = node.name
|
|
81
|
-
|
|
82
|
-
self.global_names.clear()
|
|
83
|
-
self.nonlocal_names.clear()
|
|
84
|
-
|
|
85
|
-
self.scope_stack.append({})
|
|
86
|
-
|
|
87
|
-
for arg in node.args.args:
|
|
88
|
-
self.define_var(arg.arg, arg, is_parameter=True)
|
|
89
|
-
|
|
90
|
-
if node.args.vararg:
|
|
91
|
-
self.define_var(node.args.vararg.arg, node.args.vararg, is_parameter=True)
|
|
92
|
-
if node.args.kwarg:
|
|
93
|
-
self.define_var(node.args.kwarg.arg, node.args.kwarg, is_parameter=True)
|
|
94
|
-
|
|
95
|
-
for i, default in enumerate(node.args.defaults):
|
|
96
|
-
arg_index = len(node.args.args) - len(node.args.defaults) + i
|
|
97
|
-
if arg_index < len(node.args.args):
|
|
98
|
-
arg_name = node.args.args[arg_index].arg
|
|
99
|
-
if arg_name in self.current_scope():
|
|
100
|
-
self.current_scope()[arg_name].is_mutable_default = self._is_mutable(default)
|
|
101
|
-
self.current_scope()[arg_name].default_node = default
|
|
102
|
-
|
|
103
|
-
self.generic_visit(node)
|
|
104
|
-
|
|
105
|
-
self._check_function_scope(self.current_scope(), node.name)
|
|
106
|
-
self.scope_stack.pop()
|
|
107
|
-
self.current_function = old_function
|
|
108
|
-
|
|
109
|
-
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
|
|
110
|
-
self.visit_FunctionDef(node)
|
|
111
|
-
|
|
112
|
-
def visit_ClassDef(self, node: ast.ClassDef):
|
|
113
|
-
self.scope_stack.append({})
|
|
114
|
-
self.generic_visit(node)
|
|
115
|
-
self.scope_stack.pop()
|
|
116
|
-
|
|
117
|
-
def visit_Global(self, node: ast.Global):
|
|
118
|
-
for name in node.names:
|
|
119
|
-
self.global_names.add(name)
|
|
120
|
-
for scope in reversed(self.scope_stack):
|
|
121
|
-
if name in scope:
|
|
122
|
-
scope[name].is_global = True
|
|
123
|
-
break
|
|
124
|
-
|
|
125
|
-
def visit_Nonlocal(self, node: ast.Nonlocal):
|
|
126
|
-
for name in node.names:
|
|
127
|
-
self.nonlocal_names.add(name)
|
|
128
|
-
for scope in reversed(self.scope_stack[:-1]):
|
|
129
|
-
if name in scope:
|
|
130
|
-
scope[name].is_nonlocal = True
|
|
131
|
-
break
|
|
132
|
-
|
|
133
|
-
def visit_Assign(self, node: ast.Assign):
|
|
134
|
-
for target in node.targets:
|
|
135
|
-
names = self._get_assignment_names(target)
|
|
136
|
-
for name in names:
|
|
137
|
-
self.define_var(name, node)
|
|
138
|
-
self.assignment_targets[node.lineno].append(name)
|
|
139
|
-
self.generic_visit(node.value)
|
|
140
|
-
|
|
141
|
-
def visit_AnnAssign(self, node: ast.AnnAssign):
|
|
142
|
-
if node.target:
|
|
143
|
-
names = self._get_assignment_names(node.target)
|
|
144
|
-
for name in names:
|
|
145
|
-
self.define_var(name, node)
|
|
146
|
-
self.assignment_targets[node.lineno].append(name)
|
|
147
|
-
if node.value:
|
|
148
|
-
self.generic_visit(node.value)
|
|
149
|
-
|
|
150
|
-
def visit_AugAssign(self, node: ast.AugAssign):
|
|
151
|
-
names = self._get_assignment_names(node.target)
|
|
152
|
-
for name in names:
|
|
153
|
-
self.define_var(name, node)
|
|
154
|
-
self.use_var(name, node)
|
|
155
|
-
self.assignment_targets[node.lineno].append(name)
|
|
156
|
-
self.generic_visit(node.value)
|
|
157
|
-
|
|
158
|
-
def _get_assignment_names(self, target: ast.AST) -> List[str]:
|
|
159
|
-
names = []
|
|
160
|
-
if isinstance(target, ast.Name):
|
|
161
|
-
names.append(target.id)
|
|
162
|
-
elif isinstance(target, (ast.Tuple, ast.List)):
|
|
163
|
-
for elt in target.elts:
|
|
164
|
-
names.extend(self._get_assignment_names(elt))
|
|
165
|
-
elif isinstance(target, ast.Attribute):
|
|
166
|
-
pass
|
|
167
|
-
elif isinstance(target, ast.Subscript):
|
|
168
|
-
pass
|
|
169
|
-
return names
|
|
170
|
-
|
|
171
|
-
def visit_Name(self, node: ast.Name):
|
|
172
|
-
if isinstance(node.ctx, ast.Load):
|
|
173
|
-
self.use_var(node.id, node)
|
|
174
|
-
elif isinstance(node.ctx, ast.Store):
|
|
175
|
-
self.define_var(node.id, node)
|
|
176
|
-
elif isinstance(node.ctx, ast.Del):
|
|
177
|
-
pass
|
|
178
|
-
self.generic_visit(node)
|
|
179
|
-
|
|
180
|
-
def visit_For(self, node: ast.For):
|
|
181
|
-
self.loop_stack.append(node.lineno)
|
|
182
|
-
names = self._get_assignment_names(node.target)
|
|
183
|
-
for name in names:
|
|
184
|
-
self.define_var(name, node)
|
|
185
|
-
self.generic_visit(node.iter)
|
|
186
|
-
for stmt in node.body:
|
|
187
|
-
self.generic_visit(stmt)
|
|
188
|
-
for stmt in node.orelse:
|
|
189
|
-
self.generic_visit(stmt)
|
|
190
|
-
self.loop_stack.pop()
|
|
191
|
-
|
|
192
|
-
def visit_While(self, node: ast.While):
|
|
193
|
-
self.loop_stack.append(node.lineno)
|
|
194
|
-
self.generic_visit(node.test)
|
|
195
|
-
for stmt in node.body:
|
|
196
|
-
self.generic_visit(stmt)
|
|
197
|
-
for stmt in node.orelse:
|
|
198
|
-
self.generic_visit(stmt)
|
|
199
|
-
self.loop_stack.pop()
|
|
200
|
-
|
|
201
|
-
def visit_With(self, node: ast.With):
|
|
202
|
-
for item in node.items:
|
|
203
|
-
if item.optional_vars:
|
|
204
|
-
names = self._get_assignment_names(item.optional_vars)
|
|
205
|
-
for name in names:
|
|
206
|
-
self.define_var(name, node)
|
|
207
|
-
for stmt in node.body:
|
|
208
|
-
self.generic_visit(stmt)
|
|
209
|
-
|
|
210
|
-
def visit_Try(self, node: ast.Try):
|
|
211
|
-
for handler in node.handlers:
|
|
212
|
-
if handler.name:
|
|
213
|
-
self.define_var(handler.name, handler)
|
|
214
|
-
for stmt in node.body:
|
|
215
|
-
self.generic_visit(stmt)
|
|
216
|
-
for handler in node.handlers:
|
|
217
|
-
for stmt in handler.body:
|
|
218
|
-
self.generic_visit(stmt)
|
|
219
|
-
for stmt in node.orelse:
|
|
220
|
-
self.generic_visit(stmt)
|
|
221
|
-
for stmt in node.finalbody:
|
|
222
|
-
self.generic_visit(stmt)
|
|
223
|
-
|
|
224
|
-
def visit_Comprehension(self, node: ast.comprehension):
|
|
225
|
-
names = self._get_assignment_names(node.target)
|
|
226
|
-
for name in names:
|
|
227
|
-
self.define_var(name, node)
|
|
228
|
-
self.generic_visit(node.iter)
|
|
229
|
-
for if_clause in node.ifs:
|
|
230
|
-
self.generic_visit(if_clause)
|
|
231
|
-
|
|
232
|
-
def visit_Lambda(self, node: ast.Lambda):
|
|
233
|
-
old_function = self.current_function
|
|
234
|
-
self.current_function = "<lambda>"
|
|
235
|
-
self.scope_stack.append({})
|
|
236
|
-
for arg in node.args.args:
|
|
237
|
-
self.define_var(arg.arg, arg, is_parameter=True)
|
|
238
|
-
self.generic_visit(node.body)
|
|
239
|
-
self._check_function_scope(self.current_scope(), "<lambda>")
|
|
240
|
-
self.scope_stack.pop()
|
|
241
|
-
self.current_function = old_function
|
|
242
|
-
|
|
243
|
-
def _is_mutable(self, node: ast.AST) -> bool:
|
|
244
|
-
if isinstance(node, (ast.List, ast.Dict, ast.Set)):
|
|
245
|
-
return True
|
|
246
|
-
if isinstance(node, ast.Call):
|
|
247
|
-
if isinstance(node.func, ast.Name):
|
|
248
|
-
return node.func.id in ("list", "dict", "set", "bytearray", "deque", "defaultdict")
|
|
249
|
-
if isinstance(node.func, ast.Attribute):
|
|
250
|
-
return node.func.attr in ("list", "dict", "set", "copy", "deepcopy")
|
|
251
|
-
return False
|
|
252
|
-
|
|
253
|
-
def _check_use_before_def(self, var: VarInfo, func_name: str):
|
|
254
|
-
for use_line in var.used_at:
|
|
255
|
-
has_def_before = any(def_line < use_line for def_line in var.defined_at)
|
|
256
|
-
if not has_def_before and not var.is_parameter:
|
|
257
|
-
self.findings.append(Finding(
|
|
258
|
-
engine="dataflow",
|
|
259
|
-
severity="medium",
|
|
260
|
-
title="Use before definition",
|
|
261
|
-
description=f"Variable '{var.name}' used before assignment in function '{func_name}'",
|
|
262
|
-
location=f"{self.filename}:{use_line}",
|
|
263
|
-
metadata={
|
|
264
|
-
"snippet": self.get_snippet_by_line(use_line),
|
|
265
|
-
"cwe": "CWE-457",
|
|
266
|
-
"confidence": 0.85,
|
|
267
|
-
"check": "use_before_def",
|
|
268
|
-
"variable": var.name,
|
|
269
|
-
"function": func_name,
|
|
270
|
-
}
|
|
271
|
-
))
|
|
272
|
-
|
|
273
|
-
def _check_function_scope(self, scope: Dict[str, VarInfo], func_name: str):
|
|
274
|
-
for var in scope.values():
|
|
275
|
-
self._check_use_before_def(var, func_name)
|
|
276
|
-
if var.is_mutable_default and var.default_node:
|
|
277
|
-
self.findings.append(Finding(
|
|
278
|
-
engine="dataflow",
|
|
279
|
-
severity="medium",
|
|
280
|
-
title="Mutable default argument",
|
|
281
|
-
description=f"Function '{func_name}' has mutable default argument '{var.name}' "
|
|
282
|
-
f"which is shared across all calls",
|
|
283
|
-
location=f"{self.filename}:{var.default_node.lineno}",
|
|
284
|
-
metadata={
|
|
285
|
-
"snippet": self.get_snippet(var.default_node),
|
|
286
|
-
"cwe": "CWE-477",
|
|
287
|
-
"confidence": 0.95,
|
|
288
|
-
"check": "mutable_default",
|
|
289
|
-
"variable": var.name,
|
|
290
|
-
"function": func_name,
|
|
291
|
-
}
|
|
292
|
-
))
|
|
293
|
-
|
|
294
|
-
def _check_dead_stores(self):
|
|
295
|
-
for scope in self.scope_stack:
|
|
296
|
-
for var in scope.values():
|
|
297
|
-
# Only check local function variables, not module level or global exports
|
|
298
|
-
if var.scope_level > 1 and var.assigned_at and not var.used_at and not var.is_parameter:
|
|
299
|
-
if var.name.startswith("_") or var.name in ("self", "cls"):
|
|
300
|
-
continue
|
|
301
|
-
for line in var.assigned_at:
|
|
302
|
-
self.findings.append(Finding(
|
|
303
|
-
engine="dataflow",
|
|
304
|
-
severity="low",
|
|
305
|
-
title="Dead store",
|
|
306
|
-
description=f"Local variable '{var.name}' is assigned but never used",
|
|
307
|
-
location=f"{self.filename}:{line}",
|
|
308
|
-
metadata={
|
|
309
|
-
"snippet": self.get_snippet_by_line(line),
|
|
310
|
-
"cwe": "CWE-563",
|
|
311
|
-
"confidence": 0.8,
|
|
312
|
-
"check": "dead_store",
|
|
313
|
-
"variable": var.name,
|
|
314
|
-
}
|
|
315
|
-
))
|
|
316
|
-
|
|
317
|
-
def _check_mutable_defaults(self):
|
|
318
|
-
pass
|
|
319
|
-
|
|
320
|
-
def _check_global_mutations(self):
|
|
321
|
-
for scope in self.scope_stack:
|
|
322
|
-
for var in scope.values():
|
|
323
|
-
if var.is_global and var.assigned_at and self.current_function:
|
|
324
|
-
if var.name not in ("__all__", "__version__", "__author__", "__name__"):
|
|
325
|
-
for line in var.assigned_at:
|
|
326
|
-
self.findings.append(Finding(
|
|
327
|
-
engine="dataflow",
|
|
328
|
-
severity="low",
|
|
329
|
-
title="Global variable mutation",
|
|
330
|
-
description=f"Global variable '{var.name}' is modified in function '{self.current_function}'",
|
|
331
|
-
location=f"{self.filename}:{line}",
|
|
332
|
-
metadata={
|
|
333
|
-
"snippet": self.get_snippet_by_line(line),
|
|
334
|
-
"cwe": "CWE-473",
|
|
335
|
-
"confidence": 0.75,
|
|
336
|
-
"check": "global_mutation",
|
|
337
|
-
"variable": var.name,
|
|
338
|
-
"function": self.current_function,
|
|
339
|
-
}
|
|
340
|
-
))
|
|
341
|
-
|
|
342
|
-
def get_snippet_by_line(self, line: int, context: int = 2) -> str:
|
|
343
|
-
start = max(0, line - 1 - context)
|
|
344
|
-
end = min(len(self.source_lines), line + context)
|
|
345
|
-
return "\n".join(self.source_lines[start:end])
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
class Engine:
|
|
349
|
-
name = "dataflow"
|
|
350
|
-
version = "1.0.0"
|
|
351
|
-
|
|
352
|
-
async def check_availability(self) -> bool:
|
|
353
|
-
return True
|
|
354
|
-
|
|
355
|
-
def resource_limits(self) -> ResourceLimits:
|
|
356
|
-
return ResourceLimits(max_cpu_seconds=60, max_memory_mb=512, max_files=50000)
|
|
357
|
-
|
|
358
|
-
async def run(self, ctx, config: EngineConfig) -> EngineResult:
|
|
359
|
-
# Handle both single IR and list of IRs
|
|
360
|
-
if not isinstance(ctx, list):
|
|
361
|
-
ctx = [ctx]
|
|
362
|
-
all_findings = []
|
|
363
|
-
errors = []
|
|
364
|
-
files_processed = 0
|
|
365
|
-
|
|
366
|
-
exclude_patterns = config.toggles.get("exclude", []) if config.toggles else []
|
|
367
|
-
|
|
368
|
-
for ir in ctx:
|
|
369
|
-
try:
|
|
370
|
-
if not ir.metadata.get("language") == "python":
|
|
371
|
-
continue
|
|
372
|
-
|
|
373
|
-
rel_path = ir.metadata.get("path", "unknown.py")
|
|
374
|
-
if any(pattern in rel_path for pattern in exclude_patterns):
|
|
375
|
-
continue
|
|
376
|
-
|
|
377
|
-
content = ir.content.decode("utf-8", errors="replace")
|
|
378
|
-
source_lines = content.splitlines()
|
|
379
|
-
tree = ast.parse(content, filename=rel_path)
|
|
380
|
-
|
|
381
|
-
visitor = DataFlowVisitor(rel_path, source_lines)
|
|
382
|
-
visitor.visit(tree)
|
|
383
|
-
|
|
384
|
-
all_findings.extend(visitor.findings)
|
|
385
|
-
files_processed += 1
|
|
386
|
-
|
|
387
|
-
except SyntaxError as e:
|
|
388
|
-
errors.append(f"Syntax error in {ir.metadata.get('path', 'unknown')}: {e}")
|
|
389
|
-
except Exception as e:
|
|
390
|
-
errors.append(f"Error processing {ir.metadata.get('path', 'unknown')}: {e}")
|
|
391
|
-
|
|
392
|
-
return EngineResult(
|
|
393
|
-
engine=self.name,
|
|
394
|
-
findings=all_findings,
|
|
395
|
-
errors=errors,
|
|
396
|
-
metrics={"files_processed": files_processed, "findings_count": len(all_findings)},
|
|
397
|
-
ok=len(errors) == 0
|
|
398
|
-
)
|
|
Binary file
|