@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.
Files changed (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +248 -0
  3. package/assets/logo.png +0 -0
  4. package/bin/orion.js +48 -0
  5. package/docs/API_REFERENCE.md +211 -0
  6. package/docs/ARCHITECTURE.md +183 -0
  7. package/docs/CHANGELOG.md +112 -0
  8. package/docs/CONTRIBUTING.md +157 -0
  9. package/docs/ENGINE_CATALOG.md +87 -0
  10. package/docs/GETTING_STARTED.md +226 -0
  11. package/package.json +48 -0
  12. package/pyproject.toml +16 -0
  13. package/src/orion/__init__.py +0 -0
  14. package/src/orion/__pycache__/__init__.cpython-313.pyc +0 -0
  15. package/src/orion/engine/__init__.py +17 -0
  16. package/src/orion/engine/__pycache__/__init__.cpython-313.pyc +0 -0
  17. package/src/orion/engine/__pycache__/config.cpython-313.pyc +0 -0
  18. package/src/orion/engine/__pycache__/core.cpython-313.pyc +0 -0
  19. package/src/orion/engine/__pycache__/ir.cpython-313.pyc +0 -0
  20. package/src/orion/engine/__pycache__/languages.cpython-313.pyc +0 -0
  21. package/src/orion/engine/__pycache__/registry.cpython-313.pyc +0 -0
  22. package/src/orion/engine/__pycache__/results.cpython-313.pyc +0 -0
  23. package/src/orion/engine/__pycache__/runner.cpython-313.pyc +0 -0
  24. package/src/orion/engine/api_security/__pycache__/engine.cpython-313.pyc +0 -0
  25. package/src/orion/engine/api_security/engine.py +125 -0
  26. package/src/orion/engine/architecture/__pycache__/engine.cpython-313.pyc +0 -0
  27. package/src/orion/engine/architecture/engine.py +463 -0
  28. package/src/orion/engine/authorization/__pycache__/engine.cpython-313.pyc +0 -0
  29. package/src/orion/engine/authorization/engine.py +337 -0
  30. package/src/orion/engine/callgraph/__pycache__/engine.cpython-313.pyc +0 -0
  31. package/src/orion/engine/callgraph/engine.py +236 -0
  32. package/src/orion/engine/cfg/__pycache__/engine.cpython-313.pyc +0 -0
  33. package/src/orion/engine/cfg/engine.py +509 -0
  34. package/src/orion/engine/chaos/__pycache__/engine.cpython-313.pyc +0 -0
  35. package/src/orion/engine/chaos/engine.py +268 -0
  36. package/src/orion/engine/chaos_config/__pycache__/engine.cpython-313.pyc +0 -0
  37. package/src/orion/engine/chaos_config/engine.py +322 -0
  38. package/src/orion/engine/complexity/__pycache__/engine.cpython-313.pyc +0 -0
  39. package/src/orion/engine/complexity/engine.py +144 -0
  40. package/src/orion/engine/compliance/__pycache__/engine.cpython-313.pyc +0 -0
  41. package/src/orion/engine/compliance/engine.py +325 -0
  42. package/src/orion/engine/config.py +41 -0
  43. package/src/orion/engine/config_engine/__pycache__/engine.cpython-313.pyc +0 -0
  44. package/src/orion/engine/config_engine/engine.py +364 -0
  45. package/src/orion/engine/config_validation/__pycache__/engine.cpython-313.pyc +0 -0
  46. package/src/orion/engine/config_validation/engine.py +313 -0
  47. package/src/orion/engine/container/__pycache__/engine.cpython-313.pyc +0 -0
  48. package/src/orion/engine/container/engine.py +546 -0
  49. package/src/orion/engine/contract_probe/__pycache__/engine.cpython-313.pyc +0 -0
  50. package/src/orion/engine/contract_probe/engine.py +316 -0
  51. package/src/orion/engine/core.py +75 -0
  52. package/src/orion/engine/crypto_guard/__pycache__/engine.cpython-313.pyc +0 -0
  53. package/src/orion/engine/crypto_guard/engine.py +134 -0
  54. package/src/orion/engine/data_residency/__pycache__/engine.cpython-313.pyc +0 -0
  55. package/src/orion/engine/data_residency/engine.py +238 -0
  56. package/src/orion/engine/dataflow/__pycache__/engine.cpython-313.pyc +0 -0
  57. package/src/orion/engine/dataflow/engine.py +398 -0
  58. package/src/orion/engine/dead_import/__pycache__/engine.cpython-313.pyc +0 -0
  59. package/src/orion/engine/dead_import/engine.py +293 -0
  60. package/src/orion/engine/deps/__pycache__/engine.cpython-313.pyc +0 -0
  61. package/src/orion/engine/deps/engine.py +244 -0
  62. package/src/orion/engine/dispatcher.py +350 -0
  63. package/src/orion/engine/docs/AUDIT_PROMPT.md +1248 -0
  64. package/src/orion/engine/duplication/__pycache__/engine.cpython-313.pyc +0 -0
  65. package/src/orion/engine/duplication/engine.py +120 -0
  66. package/src/orion/engine/error_handling/__pycache__/engine.cpython-313.pyc +0 -0
  67. package/src/orion/engine/error_handling/engine.py +256 -0
  68. package/src/orion/engine/fp_learn/__pycache__/engine.cpython-313.pyc +0 -0
  69. package/src/orion/engine/fp_learn/engine.py +248 -0
  70. package/src/orion/engine/hunter/__pycache__/engine.cpython-313.pyc +0 -0
  71. package/src/orion/engine/hunter/engine.py +324 -0
  72. package/src/orion/engine/iac_security/__pycache__/engine.cpython-313.pyc +0 -0
  73. package/src/orion/engine/iac_security/engine.py +218 -0
  74. package/src/orion/engine/ir.py +22 -0
  75. package/src/orion/engine/languages.py +454 -0
  76. package/src/orion/engine/license/__pycache__/engine.cpython-313.pyc +0 -0
  77. package/src/orion/engine/license/engine.py +172 -0
  78. package/src/orion/engine/lint/__pycache__/engine.cpython-313.pyc +0 -0
  79. package/src/orion/engine/lint/engine.py +558 -0
  80. package/src/orion/engine/llm_security/__pycache__/engine.cpython-313.pyc +0 -0
  81. package/src/orion/engine/llm_security/engine.py +125 -0
  82. package/src/orion/engine/logging_c/__pycache__/engine.cpython-313.pyc +0 -0
  83. package/src/orion/engine/logging_c/engine.py +164 -0
  84. package/src/orion/engine/noise/__pycache__/engine.cpython-313.pyc +0 -0
  85. package/src/orion/engine/noise/engine.py +140 -0
  86. package/src/orion/engine/orphan/__pycache__/engine.cpython-313.pyc +0 -0
  87. package/src/orion/engine/orphan/engine.py +244 -0
  88. package/src/orion/engine/perf_guard/__pycache__/engine.cpython-313.pyc +0 -0
  89. package/src/orion/engine/perf_guard/engine.py +345 -0
  90. package/src/orion/engine/prompt_security/__pycache__/engine.cpython-313.pyc +0 -0
  91. package/src/orion/engine/prompt_security/detectors/__init__.py +4 -0
  92. package/src/orion/engine/prompt_security/engine.py +170 -0
  93. package/src/orion/engine/provenance/__pycache__/engine.cpython-313.pyc +0 -0
  94. package/src/orion/engine/provenance/engine.py +242 -0
  95. package/src/orion/engine/registry.py +55 -0
  96. package/src/orion/engine/regression/__pycache__/engine.cpython-313.pyc +0 -0
  97. package/src/orion/engine/regression/engine.py +213 -0
  98. package/src/orion/engine/results.py +21 -0
  99. package/src/orion/engine/runner.py +1310 -0
  100. package/src/orion/engine/sandbox_escape/__pycache__/engine.cpython-313.pyc +0 -0
  101. package/src/orion/engine/sandbox_escape/engine.py +284 -0
  102. package/src/orion/engine/sast/__pycache__/engine.cpython-313.pyc +0 -0
  103. package/src/orion/engine/sast/engine.py +217 -0
  104. package/src/orion/engine/sast/patterns/__init__.py +513 -0
  105. package/src/orion/engine/sast/patterns/__pycache__/__init__.cpython-313.pyc +0 -0
  106. package/src/orion/engine/secrets/__pycache__/engine.cpython-313.pyc +0 -0
  107. package/src/orion/engine/secrets/engine.py +77 -0
  108. package/src/orion/engine/secrets/patterns/__init__.py +74 -0
  109. package/src/orion/engine/secrets/patterns/__pycache__/__init__.cpython-313.pyc +0 -0
  110. package/src/orion/engine/sensitive_data/__pycache__/engine.cpython-313.pyc +0 -0
  111. package/src/orion/engine/sensitive_data/engine.py +143 -0
  112. package/src/orion/engine/state/__pycache__/engine.cpython-313.pyc +0 -0
  113. package/src/orion/engine/state/engine.py +218 -0
  114. package/src/orion/engine/supply_chain_sbom/__pycache__/engine.cpython-313.pyc +0 -0
  115. package/src/orion/engine/supply_chain_sbom/engine.py +290 -0
  116. package/src/orion/engine/symbol/__pycache__/engine.cpython-313.pyc +0 -0
  117. package/src/orion/engine/symbol/engine.py +463 -0
  118. package/src/orion/engine/taint/__pycache__/engine.cpython-313.pyc +0 -0
  119. package/src/orion/engine/taint/engine.py +464 -0
  120. package/src/orion/engine/test_coverage/__pycache__/engine.cpython-313.pyc +0 -0
  121. package/src/orion/engine/test_coverage/engine.py +445 -0
  122. package/src/orion/engine/threat_model/__pycache__/engine.cpython-313.pyc +0 -0
  123. package/src/orion/engine/threat_model/engine.py +126 -0
  124. package/src/orion/engine/wiring_trace/__pycache__/engine.cpython-313.pyc +0 -0
  125. package/src/orion/engine/wiring_trace/engine.py +200 -0
  126. package/src/orion_engine.egg-info/PKG-INFO +7 -0
  127. package/src/orion_engine.egg-info/SOURCES.txt +65 -0
  128. package/src/orion_engine.egg-info/dependency_links.txt +1 -0
  129. package/src/orion_engine.egg-info/entry_points.txt +2 -0
  130. 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
+ )