@orionscanner/cli 0.1.0
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/LICENSE +21 -0
- package/README.md +248 -0
- package/assets/logo.png +0 -0
- package/bin/orion.js +48 -0
- package/docs/API_REFERENCE.md +211 -0
- package/docs/ARCHITECTURE.md +183 -0
- package/docs/CHANGELOG.md +112 -0
- package/docs/CONTRIBUTING.md +157 -0
- package/docs/ENGINE_CATALOG.md +87 -0
- package/docs/GETTING_STARTED.md +226 -0
- package/package.json +48 -0
- package/pyproject.toml +16 -0
- package/src/orion/__init__.py +0 -0
- package/src/orion/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/orion/engine/__init__.py +17 -0
- 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 +125 -0
- package/src/orion/engine/architecture/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/architecture/engine.py +463 -0
- package/src/orion/engine/authorization/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/authorization/engine.py +337 -0
- package/src/orion/engine/callgraph/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/callgraph/engine.py +236 -0
- package/src/orion/engine/cfg/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/cfg/engine.py +509 -0
- package/src/orion/engine/chaos/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/chaos/engine.py +268 -0
- package/src/orion/engine/chaos_config/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/chaos_config/engine.py +322 -0
- package/src/orion/engine/complexity/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/complexity/engine.py +144 -0
- package/src/orion/engine/compliance/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/compliance/engine.py +325 -0
- package/src/orion/engine/config.py +41 -0
- package/src/orion/engine/config_engine/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/config_engine/engine.py +364 -0
- package/src/orion/engine/config_validation/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/config_validation/engine.py +313 -0
- package/src/orion/engine/container/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/container/engine.py +546 -0
- package/src/orion/engine/contract_probe/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/contract_probe/engine.py +316 -0
- package/src/orion/engine/core.py +75 -0
- package/src/orion/engine/crypto_guard/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/crypto_guard/engine.py +134 -0
- package/src/orion/engine/data_residency/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/data_residency/engine.py +238 -0
- package/src/orion/engine/dataflow/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/dataflow/engine.py +398 -0
- package/src/orion/engine/dead_import/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/dead_import/engine.py +293 -0
- package/src/orion/engine/deps/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/deps/engine.py +244 -0
- package/src/orion/engine/dispatcher.py +350 -0
- package/src/orion/engine/docs/AUDIT_PROMPT.md +1248 -0
- package/src/orion/engine/duplication/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/duplication/engine.py +120 -0
- package/src/orion/engine/error_handling/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/error_handling/engine.py +256 -0
- package/src/orion/engine/fp_learn/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/fp_learn/engine.py +248 -0
- package/src/orion/engine/hunter/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/hunter/engine.py +324 -0
- package/src/orion/engine/iac_security/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/iac_security/engine.py +218 -0
- package/src/orion/engine/ir.py +22 -0
- package/src/orion/engine/languages.py +454 -0
- package/src/orion/engine/license/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/license/engine.py +172 -0
- package/src/orion/engine/lint/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/lint/engine.py +558 -0
- package/src/orion/engine/llm_security/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/llm_security/engine.py +125 -0
- package/src/orion/engine/logging_c/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/logging_c/engine.py +164 -0
- package/src/orion/engine/noise/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/noise/engine.py +140 -0
- package/src/orion/engine/orphan/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/orphan/engine.py +244 -0
- package/src/orion/engine/perf_guard/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/perf_guard/engine.py +345 -0
- package/src/orion/engine/prompt_security/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/prompt_security/detectors/__init__.py +4 -0
- package/src/orion/engine/prompt_security/engine.py +170 -0
- package/src/orion/engine/provenance/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/provenance/engine.py +242 -0
- package/src/orion/engine/registry.py +55 -0
- package/src/orion/engine/regression/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/regression/engine.py +213 -0
- package/src/orion/engine/results.py +21 -0
- package/src/orion/engine/runner.py +1310 -0
- package/src/orion/engine/sandbox_escape/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/sandbox_escape/engine.py +284 -0
- package/src/orion/engine/sast/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/sast/engine.py +217 -0
- package/src/orion/engine/sast/patterns/__init__.py +513 -0
- 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 +77 -0
- package/src/orion/engine/secrets/patterns/__init__.py +74 -0
- 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 +143 -0
- package/src/orion/engine/state/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/state/engine.py +218 -0
- package/src/orion/engine/supply_chain_sbom/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/supply_chain_sbom/engine.py +290 -0
- package/src/orion/engine/symbol/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/symbol/engine.py +463 -0
- package/src/orion/engine/taint/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/taint/engine.py +464 -0
- package/src/orion/engine/test_coverage/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/test_coverage/engine.py +445 -0
- package/src/orion/engine/threat_model/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/threat_model/engine.py +126 -0
- package/src/orion/engine/wiring_trace/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/wiring_trace/engine.py +200 -0
- package/src/orion_engine.egg-info/PKG-INFO +7 -0
- package/src/orion_engine.egg-info/SOURCES.txt +65 -0
- package/src/orion_engine.egg-info/dependency_links.txt +1 -0
- package/src/orion_engine.egg-info/entry_points.txt +2 -0
- package/src/orion_engine.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
"""AST-based taint tracking engine for detecting user input flowing to dangerous sinks."""
|
|
2
|
+
import ast
|
|
3
|
+
import asyncio
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Dict, List, Set, Optional, Any
|
|
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 TaintState:
|
|
13
|
+
tainted_vars: Set[str] = field(default_factory=set)
|
|
14
|
+
tainted_attrs: Dict[str, Set[str]] = field(default_factory=dict)
|
|
15
|
+
tainted_indices: Dict[str, Set[Any]] = field(default_factory=dict)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
SOURCE_PATTERNS = {
|
|
19
|
+
"request": ["request.args", "request.form", "request.json", "request.files",
|
|
20
|
+
"request.values", "request.data", "request.get_json",
|
|
21
|
+
"request.GET", "request.POST", "request.query_params", "request.body"],
|
|
22
|
+
"input": ["input(", "sys.stdin", "sys.argv", "os.environ"],
|
|
23
|
+
"flask": ["request.args", "request.form", "request.json", "request.files",
|
|
24
|
+
"request.values", "request.data", "request.get_json"],
|
|
25
|
+
"fastapi": ["request.query_params", "request.body", "request.form", "request.json"],
|
|
26
|
+
"django": ["request.GET", "request.POST", "request.body", "request.FILES",
|
|
27
|
+
"request.COOKIES", "request.META"],
|
|
28
|
+
"tornado": ["self.get_argument", "self.get_arguments", "self.request.arguments",
|
|
29
|
+
"self.request.body"],
|
|
30
|
+
"bottle": ["request.query", "request.forms", "request.json", "request.files"],
|
|
31
|
+
"cherrypy": ["cherrypy.request.params", "cherrypy.request.body"],
|
|
32
|
+
"web": ["self.args", "self.form", "self.json", "self.request.query",
|
|
33
|
+
"self.request.form", "self.request.json"],
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
SINK_PATTERNS = {
|
|
37
|
+
"sql": ["execute", "executemany", "raw", "cursor", "executescript"],
|
|
38
|
+
"command": ["system", "popen", "subprocess.call", "subprocess.run",
|
|
39
|
+
"subprocess.Popen", "os.system", "os.popen", "commands.getstatusoutput",
|
|
40
|
+
"commands.getoutput"],
|
|
41
|
+
"code_exec": ["eval", "exec", "compile", "__import__", "execfile"],
|
|
42
|
+
"template": ["render_template_string", "render_template", "Markup", "MarkupSafe",
|
|
43
|
+
"Template", "Environment", "jinja2.Environment"],
|
|
44
|
+
"file": ["open", "file", "io.open", "pathlib.Path.open"],
|
|
45
|
+
"deserialize": ["pickle.loads", "pickle.load", "yaml.load", "yaml.unsafe_load",
|
|
46
|
+
"marshmallow.load", "json.loads"],
|
|
47
|
+
"ldap": ["ldap.initialize", "ldap.search", "ldap.bind"],
|
|
48
|
+
"xpath": ["etree.XPath", "lxml.etree.XPath"],
|
|
49
|
+
"regex": ["re.compile", "re.search", "re.match", "re.findall"],
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
SANITIZER_PATTERNS = {
|
|
53
|
+
"sql": ["escape_string", "quote", "quote_identifier", "mogrify",
|
|
54
|
+
"parameterize", "cursor.execute", "text(", "bindparam"],
|
|
55
|
+
"html": ["escape", "html_escape", "escapehtml", "markupsafe.escape",
|
|
56
|
+
"cgi.escape", "html.escape", "bleach.clean"],
|
|
57
|
+
"shell": ["shlex.quote", "pipes.quote", "subprocess.list2cmdline"],
|
|
58
|
+
"path": ["os.path.normpath", "os.path.realpath", "pathlib.Path.resolve"],
|
|
59
|
+
"url": ["urllib.parse.quote", "urllib.parse.quote_plus", "quote", "quote_plus"],
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
TAINT_PROPAGATION_OPS = {
|
|
63
|
+
ast.Add, ast.Mult, ast.Sub, ast.Div, ast.FloorDiv, ast.Mod, ast.Pow,
|
|
64
|
+
ast.LShift, ast.RShift, ast.BitOr, ast.BitXor, ast.BitAnd,
|
|
65
|
+
ast.And, ast.Or, ast.Not, ast.UAdd, ast.USub, ast.Invert,
|
|
66
|
+
ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE,
|
|
67
|
+
ast.Is, ast.IsNot, ast.In, ast.NotIn,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class TaintVisitor(ast.NodeVisitor):
|
|
72
|
+
def __init__(self, filename: str, source_lines: List[str]):
|
|
73
|
+
self.filename = filename
|
|
74
|
+
self.source_lines = source_lines
|
|
75
|
+
self.findings: List[Finding] = []
|
|
76
|
+
self.taint_state = TaintState()
|
|
77
|
+
self.function_stack: List[str] = []
|
|
78
|
+
self.class_stack: List[str] = []
|
|
79
|
+
self.imports: Dict[str, str] = {}
|
|
80
|
+
self.current_function: Optional[str] = None
|
|
81
|
+
self.assignment_targets: Dict[str, Set[str]] = {}
|
|
82
|
+
self.call_depth = 0
|
|
83
|
+
self.max_call_depth = 50
|
|
84
|
+
|
|
85
|
+
def get_snippet(self, node: ast.AST, context: int = 2) -> str:
|
|
86
|
+
start = max(0, node.lineno - 1 - context)
|
|
87
|
+
end = min(len(self.source_lines), node.end_lineno + context if hasattr(node, 'end_lineno') and node.end_lineno else node.lineno + context)
|
|
88
|
+
return "\n".join(self.source_lines[start:end])
|
|
89
|
+
|
|
90
|
+
def is_tainted(self, node: ast.AST) -> bool:
|
|
91
|
+
if isinstance(node, ast.Name):
|
|
92
|
+
return node.id in self.taint_state.tainted_vars
|
|
93
|
+
elif isinstance(node, ast.Attribute):
|
|
94
|
+
if isinstance(node.value, ast.Name):
|
|
95
|
+
base = node.value.id
|
|
96
|
+
if base in self.taint_state.tainted_attrs:
|
|
97
|
+
return node.attr in self.taint_state.tainted_attrs[base]
|
|
98
|
+
return self.is_tainted(node.value)
|
|
99
|
+
elif isinstance(node, ast.Subscript):
|
|
100
|
+
if isinstance(node.value, ast.Name):
|
|
101
|
+
base = node.value.id
|
|
102
|
+
if base in self.taint_state.tainted_indices:
|
|
103
|
+
return True
|
|
104
|
+
return self.is_tainted(node.value)
|
|
105
|
+
elif isinstance(node, ast.BinOp):
|
|
106
|
+
return self.is_tainted(node.left) or self.is_tainted(node.right)
|
|
107
|
+
elif isinstance(node, ast.Call):
|
|
108
|
+
return self.is_tainted(node.func) or any(self.is_tainted(arg) for arg in node.args)
|
|
109
|
+
elif isinstance(node, ast.JoinedStr):
|
|
110
|
+
return any(self.is_tainted(v) for v in node.values if isinstance(v, ast.FormattedValue))
|
|
111
|
+
elif isinstance(node, ast.List):
|
|
112
|
+
return any(self.is_tainted(e) for e in node.elts)
|
|
113
|
+
elif isinstance(node, ast.Dict):
|
|
114
|
+
return any(self.is_tainted(v) for v in node.values)
|
|
115
|
+
elif isinstance(node, ast.Tuple):
|
|
116
|
+
return any(self.is_tainted(e) for e in node.elts)
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
def mark_tainted(self, target: ast.AST, source_node: Optional[ast.AST] = None):
|
|
120
|
+
if isinstance(target, ast.Name):
|
|
121
|
+
self.taint_state.tainted_vars.add(target.id)
|
|
122
|
+
elif isinstance(target, ast.Attribute):
|
|
123
|
+
if isinstance(target.value, ast.Name):
|
|
124
|
+
base = target.value.id
|
|
125
|
+
if base not in self.taint_state.tainted_attrs:
|
|
126
|
+
self.taint_state.tainted_attrs[base] = set()
|
|
127
|
+
self.taint_state.tainted_attrs[base].add(target.attr)
|
|
128
|
+
elif isinstance(target, ast.Subscript):
|
|
129
|
+
if isinstance(target.value, ast.Name):
|
|
130
|
+
base = target.value.id
|
|
131
|
+
if base not in self.taint_state.tainted_indices:
|
|
132
|
+
self.taint_state.tainted_indices[base] = set()
|
|
133
|
+
self.taint_state.tainted_indices[base].add("all")
|
|
134
|
+
elif isinstance(target, ast.Tuple):
|
|
135
|
+
for elt in target.elts:
|
|
136
|
+
self.mark_tainted(elt, source_node)
|
|
137
|
+
elif isinstance(target, ast.List):
|
|
138
|
+
for elt in target.elts:
|
|
139
|
+
self.mark_tainted(elt, source_node)
|
|
140
|
+
|
|
141
|
+
def untaint(self, target: ast.AST):
|
|
142
|
+
if isinstance(target, ast.Name):
|
|
143
|
+
self.taint_state.tainted_vars.discard(target.id)
|
|
144
|
+
elif isinstance(target, ast.Attribute):
|
|
145
|
+
if isinstance(target.value, ast.Name):
|
|
146
|
+
base = target.value.id
|
|
147
|
+
if base in self.taint_state.tainted_attrs:
|
|
148
|
+
self.taint_state.tainted_attrs[base].discard(target.attr)
|
|
149
|
+
elif isinstance(target, ast.Subscript):
|
|
150
|
+
if isinstance(target.value, ast.Name):
|
|
151
|
+
base = target.value.id
|
|
152
|
+
if base in self.taint_state.tainted_indices:
|
|
153
|
+
self.taint_state.tainted_indices[base].discard("all")
|
|
154
|
+
|
|
155
|
+
def check_source(self, node: ast.AST) -> Optional[str]:
|
|
156
|
+
if isinstance(node, ast.Call):
|
|
157
|
+
if isinstance(node.func, ast.Name):
|
|
158
|
+
if node.func.id in ["input", "eval", "exec"]:
|
|
159
|
+
return f"builtin_{node.func.id}"
|
|
160
|
+
elif isinstance(node.func, ast.Attribute):
|
|
161
|
+
attr = node.func.attr
|
|
162
|
+
if isinstance(node.func.value, ast.Name):
|
|
163
|
+
base = node.func.value.id
|
|
164
|
+
full = f"{base}.{attr}"
|
|
165
|
+
for cat, patterns in SOURCE_PATTERNS.items():
|
|
166
|
+
if any(p in full for p in patterns):
|
|
167
|
+
return f"source_{cat}"
|
|
168
|
+
elif isinstance(node, ast.Attribute):
|
|
169
|
+
if isinstance(node.value, ast.Name):
|
|
170
|
+
base = node.value.id
|
|
171
|
+
full = f"{base}.{node.attr}"
|
|
172
|
+
for cat, patterns in SOURCE_PATTERNS.items():
|
|
173
|
+
if any(p in full for p in patterns):
|
|
174
|
+
return f"source_{cat}"
|
|
175
|
+
elif isinstance(node, ast.Name):
|
|
176
|
+
if node.id in ["request", "sys.argv", "os.environ"]:
|
|
177
|
+
return "source_global"
|
|
178
|
+
elif isinstance(node, ast.Subscript):
|
|
179
|
+
if isinstance(node.value, ast.Name):
|
|
180
|
+
base = node.value.id
|
|
181
|
+
if base in ["request", "os", "sys"]:
|
|
182
|
+
return "source_subscript"
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
def check_sink(self, node: ast.Call) -> Optional[tuple]:
|
|
186
|
+
if isinstance(node.func, ast.Name):
|
|
187
|
+
func_name = node.func.id
|
|
188
|
+
for cat, patterns in SINK_PATTERNS.items():
|
|
189
|
+
if func_name in patterns:
|
|
190
|
+
return (cat, func_name)
|
|
191
|
+
elif isinstance(node.func, ast.Attribute):
|
|
192
|
+
attr = node.func.attr
|
|
193
|
+
for cat, patterns in SINK_PATTERNS.items():
|
|
194
|
+
if attr in patterns:
|
|
195
|
+
if isinstance(node.func.value, ast.Name):
|
|
196
|
+
return (cat, f"{node.func.value.id}.{attr}")
|
|
197
|
+
return (cat, attr)
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
def check_sanitizer(self, node: ast.Call) -> Optional[str]:
|
|
201
|
+
if isinstance(node.func, ast.Attribute):
|
|
202
|
+
attr = node.func.attr
|
|
203
|
+
for cat, patterns in SANITIZER_PATTERNS.items():
|
|
204
|
+
if attr in patterns:
|
|
205
|
+
return cat
|
|
206
|
+
elif isinstance(node.func, ast.Name):
|
|
207
|
+
func_name = node.func.id
|
|
208
|
+
for cat, patterns in SANITIZER_PATTERNS.items():
|
|
209
|
+
if func_name in patterns:
|
|
210
|
+
return cat
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
def visit_Import(self, node: ast.Import):
|
|
214
|
+
for alias in node.names:
|
|
215
|
+
self.imports[alias.asname or alias.name] = alias.name
|
|
216
|
+
self.generic_visit(node)
|
|
217
|
+
|
|
218
|
+
def visit_ImportFrom(self, node: ast.ImportFrom):
|
|
219
|
+
if node.module:
|
|
220
|
+
for alias in node.names:
|
|
221
|
+
self.imports[alias.asname or alias.name] = f"{node.module}.{alias.name}"
|
|
222
|
+
self.generic_visit(node)
|
|
223
|
+
|
|
224
|
+
def visit_FunctionDef(self, node: ast.FunctionDef):
|
|
225
|
+
self.function_stack.append(node.name)
|
|
226
|
+
old_function = self.current_function
|
|
227
|
+
self.current_function = node.name
|
|
228
|
+
old_taint = TaintState()
|
|
229
|
+
old_taint.tainted_vars = self.taint_state.tainted_vars.copy()
|
|
230
|
+
old_taint.tainted_attrs = {k: v.copy() for k, v in self.taint_state.tainted_attrs.items()}
|
|
231
|
+
old_taint.tainted_indices = {k: v.copy() for k, v in self.taint_state.tainted_indices.items()}
|
|
232
|
+
|
|
233
|
+
for arg in node.args.args:
|
|
234
|
+
self.taint_state.tainted_vars.discard(arg.arg)
|
|
235
|
+
self.taint_state.tainted_vars.discard(arg.arg)
|
|
236
|
+
|
|
237
|
+
self.generic_visit(node)
|
|
238
|
+
|
|
239
|
+
self.taint_state = old_taint
|
|
240
|
+
self.current_function = old_function
|
|
241
|
+
self.function_stack.pop()
|
|
242
|
+
|
|
243
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
|
|
244
|
+
self.visit_FunctionDef(node)
|
|
245
|
+
|
|
246
|
+
def visit_ClassDef(self, node: ast.ClassDef):
|
|
247
|
+
self.class_stack.append(node.name)
|
|
248
|
+
self.generic_visit(node)
|
|
249
|
+
self.class_stack.pop()
|
|
250
|
+
|
|
251
|
+
def visit_Assign(self, node: ast.Assign):
|
|
252
|
+
for target in node.targets:
|
|
253
|
+
source_tainted = self.is_tainted(node.value)
|
|
254
|
+
source_type = self.check_source(node.value)
|
|
255
|
+
|
|
256
|
+
if source_tainted or source_type:
|
|
257
|
+
self.mark_tainted(target, node.value)
|
|
258
|
+
else:
|
|
259
|
+
self.untaint(target)
|
|
260
|
+
|
|
261
|
+
if isinstance(target, ast.Name) and source_tainted:
|
|
262
|
+
self.assignment_targets.setdefault(target.id, set()).add(self.get_snippet(node))
|
|
263
|
+
|
|
264
|
+
self.generic_visit(node)
|
|
265
|
+
|
|
266
|
+
def visit_AnnAssign(self, node: ast.AnnAssign):
|
|
267
|
+
if node.value:
|
|
268
|
+
self.visit_Assign(ast.Assign(targets=[node.target], value=node.value, lineno=node.lineno))
|
|
269
|
+
self.generic_visit(node)
|
|
270
|
+
|
|
271
|
+
def visit_AugAssign(self, node: ast.AugAssign):
|
|
272
|
+
if self.is_tainted(node.value):
|
|
273
|
+
self.mark_tainted(node.target, node.value)
|
|
274
|
+
self.generic_visit(node)
|
|
275
|
+
|
|
276
|
+
def visit_Call(self, node: ast.Call):
|
|
277
|
+
sink_info = self.check_sink(node)
|
|
278
|
+
sanitizer = self.check_sanitizer(node)
|
|
279
|
+
|
|
280
|
+
args_tainted = any(self.is_tainted(arg) for arg in node.args)
|
|
281
|
+
kwargs_tainted = any(self.is_tainted(kw.value) for kw in node.keywords)
|
|
282
|
+
any_tainted = args_tainted or kwargs_tainted or self.is_tainted(node.func)
|
|
283
|
+
|
|
284
|
+
if sink_info and any_tainted:
|
|
285
|
+
cat, sink_name = sink_info
|
|
286
|
+
confidence = 0.85
|
|
287
|
+
evidence = []
|
|
288
|
+
|
|
289
|
+
if args_tainted:
|
|
290
|
+
evidence.append("tainted_positional_args")
|
|
291
|
+
if kwargs_tainted:
|
|
292
|
+
evidence.append("tainted_keyword_args")
|
|
293
|
+
if self.is_tainted(node.func):
|
|
294
|
+
evidence.append("tainted_callable")
|
|
295
|
+
|
|
296
|
+
if sanitizer:
|
|
297
|
+
confidence = 0.35
|
|
298
|
+
evidence.append(f"sanitized_by_{sanitizer}")
|
|
299
|
+
|
|
300
|
+
if len(evidence) >= 2 or (sanitizer and confidence < 0.7):
|
|
301
|
+
confidence = max(confidence, 0.7)
|
|
302
|
+
|
|
303
|
+
cwe_map = {
|
|
304
|
+
"sql": "CWE-89",
|
|
305
|
+
"command": "CWE-78",
|
|
306
|
+
"code_exec": "CWE-94",
|
|
307
|
+
"template": "CWE-79",
|
|
308
|
+
"file": "CWE-22",
|
|
309
|
+
"deserialize": "CWE-502",
|
|
310
|
+
"ldap": "CWE-90",
|
|
311
|
+
"xpath": "CWE-643",
|
|
312
|
+
"regex": "CWE-1333",
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
severity_map = {
|
|
316
|
+
"sql": "high",
|
|
317
|
+
"command": "critical",
|
|
318
|
+
"code_exec": "critical",
|
|
319
|
+
"template": "high",
|
|
320
|
+
"file": "medium",
|
|
321
|
+
"deserialize": "high",
|
|
322
|
+
"ldap": "high",
|
|
323
|
+
"xpath": "medium",
|
|
324
|
+
"regex": "medium",
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
self.findings.append(Finding(
|
|
328
|
+
engine="taint",
|
|
329
|
+
severity=severity_map.get(cat, "medium"),
|
|
330
|
+
title=f"Taint flow: user input to {cat} sink ({sink_name})",
|
|
331
|
+
description=f"Untrusted data flows into {sink_name} without proper sanitization. "
|
|
332
|
+
f"Category: {cat}. Evidence: {', '.join(evidence)}",
|
|
333
|
+
location=f"{self.filename}:{node.lineno}",
|
|
334
|
+
metadata={
|
|
335
|
+
"snippet": self.get_snippet(node),
|
|
336
|
+
"cwe": cwe_map.get(cat, "CWE-20"),
|
|
337
|
+
"confidence": confidence,
|
|
338
|
+
"sink_category": cat,
|
|
339
|
+
"sink_name": sink_name,
|
|
340
|
+
"evidence": evidence,
|
|
341
|
+
"sanitizer": sanitizer,
|
|
342
|
+
}
|
|
343
|
+
))
|
|
344
|
+
|
|
345
|
+
if sanitizer and any_tainted:
|
|
346
|
+
for arg in node.args:
|
|
347
|
+
if self.is_tainted(arg):
|
|
348
|
+
self.untaint(arg)
|
|
349
|
+
for kw in node.keywords:
|
|
350
|
+
if self.is_tainted(kw.value):
|
|
351
|
+
self.untaint(kw.value)
|
|
352
|
+
|
|
353
|
+
for arg in node.args:
|
|
354
|
+
self.visit(arg)
|
|
355
|
+
for kw in node.keywords:
|
|
356
|
+
self.visit(kw.value)
|
|
357
|
+
self.visit(node.func)
|
|
358
|
+
|
|
359
|
+
def visit_BinOp(self, node: ast.BinOp):
|
|
360
|
+
left_tainted = self.is_tainted(node.left)
|
|
361
|
+
right_tainted = self.is_tainted(node.right)
|
|
362
|
+
if left_tainted or right_tainted:
|
|
363
|
+
pass
|
|
364
|
+
self.generic_visit(node)
|
|
365
|
+
|
|
366
|
+
def visit_JoinedStr(self, node: ast.JoinedStr):
|
|
367
|
+
tainted = any(self.is_tainted(v) for v in node.values if isinstance(v, ast.FormattedValue))
|
|
368
|
+
if tainted:
|
|
369
|
+
pass
|
|
370
|
+
self.generic_visit(node)
|
|
371
|
+
|
|
372
|
+
def visit_List(self, node: ast.List):
|
|
373
|
+
tainted = any(self.is_tainted(e) for e in node.elts)
|
|
374
|
+
if tainted:
|
|
375
|
+
pass
|
|
376
|
+
self.generic_visit(node)
|
|
377
|
+
|
|
378
|
+
def visit_Dict(self, node: ast.Dict):
|
|
379
|
+
tainted = any(self.is_tainted(v) for v in node.values)
|
|
380
|
+
if tainted:
|
|
381
|
+
pass
|
|
382
|
+
self.generic_visit(node)
|
|
383
|
+
|
|
384
|
+
def visit_Return(self, node: ast.Return):
|
|
385
|
+
if node.value and self.is_tainted(node.value):
|
|
386
|
+
pass
|
|
387
|
+
self.generic_visit(node)
|
|
388
|
+
|
|
389
|
+
def visit_Yield(self, node: ast.Yield):
|
|
390
|
+
if node.value and self.is_tainted(node.value):
|
|
391
|
+
pass
|
|
392
|
+
self.generic_visit(node)
|
|
393
|
+
|
|
394
|
+
def visit_YieldFrom(self, node: ast.YieldFrom):
|
|
395
|
+
if self.is_tainted(node.value):
|
|
396
|
+
pass
|
|
397
|
+
self.generic_visit(node)
|
|
398
|
+
|
|
399
|
+
def visit_Await(self, node: ast.Await):
|
|
400
|
+
if self.is_tainted(node.value):
|
|
401
|
+
pass
|
|
402
|
+
self.generic_visit(node)
|
|
403
|
+
|
|
404
|
+
def generic_visit(self, node: ast.AST):
|
|
405
|
+
if self.call_depth > self.max_call_depth:
|
|
406
|
+
return
|
|
407
|
+
self.call_depth += 1
|
|
408
|
+
try:
|
|
409
|
+
super().generic_visit(node)
|
|
410
|
+
finally:
|
|
411
|
+
self.call_depth -= 1
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class Engine:
|
|
415
|
+
name = "taint"
|
|
416
|
+
version = "1.0.0"
|
|
417
|
+
|
|
418
|
+
async def check_availability(self) -> bool:
|
|
419
|
+
return True
|
|
420
|
+
|
|
421
|
+
def resource_limits(self) -> ResourceLimits:
|
|
422
|
+
return ResourceLimits(max_cpu_seconds=60, max_memory_mb=512, max_files=50000)
|
|
423
|
+
|
|
424
|
+
async def run(self, ctx, config: EngineConfig) -> EngineResult:
|
|
425
|
+
# Handle both single IR and list of IRs
|
|
426
|
+
if not isinstance(ctx, list):
|
|
427
|
+
ctx = [ctx]
|
|
428
|
+
all_findings = []
|
|
429
|
+
errors = []
|
|
430
|
+
files_processed = 0
|
|
431
|
+
|
|
432
|
+
exclude_patterns = config.toggles.get("exclude", []) if config.toggles else []
|
|
433
|
+
|
|
434
|
+
for ir in ctx:
|
|
435
|
+
try:
|
|
436
|
+
if not ir.metadata.get("language") == "python":
|
|
437
|
+
continue
|
|
438
|
+
|
|
439
|
+
rel_path = ir.metadata.get("path", "unknown.py")
|
|
440
|
+
if any(pattern in rel_path for pattern in exclude_patterns):
|
|
441
|
+
continue
|
|
442
|
+
|
|
443
|
+
content = ir.content.decode("utf-8", errors="replace")
|
|
444
|
+
source_lines = content.splitlines()
|
|
445
|
+
tree = ast.parse(content, filename=rel_path)
|
|
446
|
+
|
|
447
|
+
visitor = TaintVisitor(rel_path, source_lines)
|
|
448
|
+
visitor.visit(tree)
|
|
449
|
+
|
|
450
|
+
all_findings.extend(visitor.findings)
|
|
451
|
+
files_processed += 1
|
|
452
|
+
|
|
453
|
+
except SyntaxError as e:
|
|
454
|
+
errors.append(f"Syntax error in {ir.metadata.get('path', 'unknown')}: {e}")
|
|
455
|
+
except Exception as e:
|
|
456
|
+
errors.append(f"Error processing {ir.metadata.get('path', 'unknown')}: {e}")
|
|
457
|
+
|
|
458
|
+
return EngineResult(
|
|
459
|
+
engine=self.name,
|
|
460
|
+
findings=all_findings,
|
|
461
|
+
errors=errors,
|
|
462
|
+
metrics={"files_processed": files_processed, "findings_count": len(all_findings)},
|
|
463
|
+
ok=len(errors) == 0
|
|
464
|
+
)
|