@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,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)
@@ -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
- )