qaudit 1.0.0__py3-none-any.whl
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.
- qaudit/__init__.py +2 -0
- qaudit/__main__.py +4 -0
- qaudit/ast_taint.py +475 -0
- qaudit/call_graph.py +287 -0
- qaudit/checks/__init__.py +13 -0
- qaudit/checks/ast_checks.py +1287 -0
- qaudit/checks/base.py +227 -0
- qaudit/checks/code_quality.py +673 -0
- qaudit/checks/cohesion_checks.py +427 -0
- qaudit/checks/config_audit.py +687 -0
- qaudit/checks/crypto_checks.py +567 -0
- qaudit/checks/dockerfile_parser.py +119 -0
- qaudit/checks/dos_robustness.py +257 -0
- qaudit/checks/framework_checks.py +551 -0
- qaudit/checks/java_checks.py +972 -0
- qaudit/checks/java_scope_tracker.py +249 -0
- qaudit/checks/java_standards.py +1288 -0
- qaudit/checks/owasp.py +986 -0
- qaudit/checks/pii_checks.py +417 -0
- qaudit/checks/python_standards.py +780 -0
- qaudit/checks/redos_checks.py +302 -0
- qaudit/checks/secrets.py +415 -0
- qaudit/checks/security_checks.py +394 -0
- qaudit/checks/taint_checks.py +284 -0
- qaudit/checks/tools_java.py +269 -0
- qaudit/checks/tools_python.py +321 -0
- qaudit/cli.py +265 -0
- qaudit/config.py +129 -0
- qaudit/detector.py +116 -0
- qaudit/inter_taint.py +354 -0
- qaudit/models.py +84 -0
- qaudit/recommender.py +493 -0
- qaudit/reporter.py +773 -0
- qaudit/sarif_reporter.py +161 -0
- qaudit/scorer.py +183 -0
- qaudit-1.0.0.dist-info/METADATA +231 -0
- qaudit-1.0.0.dist-info/RECORD +41 -0
- qaudit-1.0.0.dist-info/WHEEL +5 -0
- qaudit-1.0.0.dist-info/entry_points.txt +2 -0
- qaudit-1.0.0.dist-info/licenses/LICENSE +21 -0
- qaudit-1.0.0.dist-info/top_level.txt +1 -0
qaudit/__init__.py
ADDED
qaudit/__main__.py
ADDED
qaudit/ast_taint.py
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Intra-function taint analysis for Python using only stdlib ast.
|
|
3
|
+
|
|
4
|
+
Algorithm
|
|
5
|
+
---------
|
|
6
|
+
For every FunctionDef / AsyncFunctionDef in a module:
|
|
7
|
+
1. Seed tainted variables from:
|
|
8
|
+
- function parameters (all args — positional, keyword, *args, **kwargs)
|
|
9
|
+
- assignments where the RHS is a known source expression
|
|
10
|
+
- extra_tainted set injected by the inter-procedural engine
|
|
11
|
+
2. Propagate taint through assignments (ordered statement walk):
|
|
12
|
+
- if any name on the RHS is tainted → LHS target is tainted
|
|
13
|
+
- if any name on the RHS is tainted AND the RHS contains a string
|
|
14
|
+
construction operation (BinOp +, JoinedStr f-string, % format,
|
|
15
|
+
.format(), str()) → LHS is also added to str_tainted
|
|
16
|
+
3. Flag when a tainted name reaches a sink call.
|
|
17
|
+
|
|
18
|
+
.execute() — two-track SQL injection detection
|
|
19
|
+
----------------------------------------------
|
|
20
|
+
SQL injection requires a *string* to be the query argument (arg[0]).
|
|
21
|
+
Two severity levels based on observed evidence:
|
|
22
|
+
|
|
23
|
+
HIGH — tainted variable reached .execute() arg[0] AND passed through a
|
|
24
|
+
string construction op (concat, f-string, % format, .format(),
|
|
25
|
+
str()). This is proven SQL injection.
|
|
26
|
+
|
|
27
|
+
MEDIUM — tainted variable reached .execute() arg[0] with NO observed string
|
|
28
|
+
construction. The variable may be a raw string from user input
|
|
29
|
+
(still dangerous) but we have less evidence — worth investigating.
|
|
30
|
+
|
|
31
|
+
silent — tainted variable only appears in .execute() arg[1+] (the params
|
|
32
|
+
tuple). This is the correct parameterised query pattern; no finding.
|
|
33
|
+
|
|
34
|
+
Sources (RHS patterns that seed taint)
|
|
35
|
+
- request.args.get / request.form.get / request.json / request.data
|
|
36
|
+
- request.GET.get / request.POST.get (Django)
|
|
37
|
+
- os.environ.get / os.getenv
|
|
38
|
+
- input()
|
|
39
|
+
- sys.argv
|
|
40
|
+
|
|
41
|
+
Sinks (calls / expressions that are dangerous with tainted input)
|
|
42
|
+
- eval(...)
|
|
43
|
+
- exec(...)
|
|
44
|
+
- os.system(...)
|
|
45
|
+
- os.popen(...)
|
|
46
|
+
- subprocess.call / run / Popen / check_output / check_call (any arg)
|
|
47
|
+
- .execute() — SQL injection (two-track, see above)
|
|
48
|
+
- compile(...)
|
|
49
|
+
- importlib.import_module(...)
|
|
50
|
+
- __import__(...)
|
|
51
|
+
|
|
52
|
+
The engine returns a list of TaintFinding objects.
|
|
53
|
+
"""
|
|
54
|
+
from __future__ import annotations
|
|
55
|
+
__author__ = "tsmishra"
|
|
56
|
+
|
|
57
|
+
import ast
|
|
58
|
+
from dataclasses import dataclass
|
|
59
|
+
from typing import Dict, List, Optional, Set
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class TaintFinding:
|
|
64
|
+
file_path: str
|
|
65
|
+
line_number: int
|
|
66
|
+
sink: str # e.g. "eval()", "os.system()"
|
|
67
|
+
source_vars: List[str] # tainted variable names that reached the sink
|
|
68
|
+
code_snippet: str
|
|
69
|
+
confidence: str = "high" # "high" | "medium" — used by .execute() two-track logic
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
# Source detection helpers
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
_SOURCE_CALLS: Set[str] = {
|
|
77
|
+
"input",
|
|
78
|
+
"request.args.get",
|
|
79
|
+
"request.form.get",
|
|
80
|
+
"request.json",
|
|
81
|
+
"request.data",
|
|
82
|
+
"request.GET.get",
|
|
83
|
+
"request.POST.get",
|
|
84
|
+
"os.environ.get",
|
|
85
|
+
"os.getenv",
|
|
86
|
+
"sys.argv",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _attr_chain(node: ast.expr) -> str:
|
|
91
|
+
"""
|
|
92
|
+
Return dotted attribute chain for simple Name/Attribute trees, else ''.
|
|
93
|
+
|
|
94
|
+
Author: tsmishra
|
|
95
|
+
"""
|
|
96
|
+
if isinstance(node, ast.Name):
|
|
97
|
+
return node.id
|
|
98
|
+
if isinstance(node, ast.Attribute):
|
|
99
|
+
parent = _attr_chain(node.value)
|
|
100
|
+
if parent:
|
|
101
|
+
return f"{parent}.{node.attr}"
|
|
102
|
+
return ""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _is_source(node: ast.expr) -> bool:
|
|
106
|
+
"""
|
|
107
|
+
True if the expression looks like a known taint source.
|
|
108
|
+
|
|
109
|
+
Author: tsmishra
|
|
110
|
+
"""
|
|
111
|
+
chain = _attr_chain(node)
|
|
112
|
+
if chain in _SOURCE_CALLS:
|
|
113
|
+
return True
|
|
114
|
+
if isinstance(node, ast.Call):
|
|
115
|
+
return _is_source(node.func)
|
|
116
|
+
if isinstance(node, ast.Subscript):
|
|
117
|
+
return _is_source(node.value)
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
# String-construction detection
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
def _is_string_construction(node: ast.expr, tainted: Set[str]) -> bool:
|
|
126
|
+
"""
|
|
127
|
+
True if *node* builds a string that incorporates at least one tainted variable.
|
|
128
|
+
|
|
129
|
+
Detected patterns:
|
|
130
|
+
tainted + "literal" BinOp with Add
|
|
131
|
+
f"...{tainted}..." JoinedStr (f-string)
|
|
132
|
+
"literal" % tainted BinOp with Mod
|
|
133
|
+
"literal".format(tainted) Call to .format()
|
|
134
|
+
str(tainted) Call to str()
|
|
135
|
+
|
|
136
|
+
Author: tsmishra
|
|
137
|
+
"""
|
|
138
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Mod)):
|
|
139
|
+
return _expr_contains_tainted(node, tainted)
|
|
140
|
+
|
|
141
|
+
if isinstance(node, ast.JoinedStr):
|
|
142
|
+
return _expr_contains_tainted(node, tainted)
|
|
143
|
+
|
|
144
|
+
if isinstance(node, ast.Call):
|
|
145
|
+
func = node.func
|
|
146
|
+
# "...".format(tainted_var)
|
|
147
|
+
if isinstance(func, ast.Attribute) and func.attr == "format":
|
|
148
|
+
return any(_expr_contains_tainted(a, tainted) for a in node.args) or \
|
|
149
|
+
any(_expr_contains_tainted(kw.value, tainted) for kw in node.keywords if kw.value)
|
|
150
|
+
# str(tainted_var)
|
|
151
|
+
if isinstance(func, ast.Name) and func.id == "str":
|
|
152
|
+
return any(_expr_contains_tainted(a, tainted) for a in node.args)
|
|
153
|
+
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# Sink detection helpers
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
_SINK_NAMES: Dict[str, str] = {
|
|
162
|
+
"eval": "eval()",
|
|
163
|
+
"exec": "exec()",
|
|
164
|
+
"compile": "compile()",
|
|
165
|
+
"__import__": "__import__()",
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
_SINK_ATTR_CHAINS: Dict[str, str] = {
|
|
169
|
+
"os.system": "os.system()",
|
|
170
|
+
"os.popen": "os.popen()",
|
|
171
|
+
"subprocess.call": "subprocess.call()",
|
|
172
|
+
"subprocess.run": "subprocess.run()",
|
|
173
|
+
"subprocess.Popen": "subprocess.Popen()",
|
|
174
|
+
"subprocess.check_output": "subprocess.check_output()",
|
|
175
|
+
"subprocess.check_call": "subprocess.check_call()",
|
|
176
|
+
"importlib.import_module": "importlib.import_module()",
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
_SINK_METHOD_NAMES: Set[str] = {"execute"}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _sink_label(call_node: ast.Call) -> Optional[str]:
|
|
183
|
+
"""Return human-readable sink label if this call is a known sink, else None."""
|
|
184
|
+
func = call_node.func
|
|
185
|
+
chain = _attr_chain(func)
|
|
186
|
+
if chain in _SINK_ATTR_CHAINS:
|
|
187
|
+
return _SINK_ATTR_CHAINS[chain]
|
|
188
|
+
if isinstance(func, ast.Name) and func.id in _SINK_NAMES:
|
|
189
|
+
return _SINK_NAMES[func.id]
|
|
190
|
+
if isinstance(func, ast.Attribute) and func.attr in _SINK_METHOD_NAMES:
|
|
191
|
+
return f".{func.attr}()"
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
# Name collection helpers
|
|
197
|
+
# ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
def _names_in_expr(node: ast.expr) -> Set[str]:
|
|
200
|
+
"""Collect all Name ids referenced in an expression."""
|
|
201
|
+
names: Set[str] = set()
|
|
202
|
+
for child in ast.walk(node):
|
|
203
|
+
if isinstance(child, ast.Name):
|
|
204
|
+
names.add(child.id)
|
|
205
|
+
return names
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _expr_contains_tainted(node: ast.expr, tainted: Set[str]) -> bool:
|
|
209
|
+
return bool(_names_in_expr(node) & tainted)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# ---------------------------------------------------------------------------
|
|
213
|
+
# Core: ordered single-pass walk of one function body
|
|
214
|
+
# ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
def _walk_stmts_ordered(stmts: list) -> List[ast.stmt]:
|
|
217
|
+
"""
|
|
218
|
+
Flatten a statement list into order-preserving depth-first sequence.
|
|
219
|
+
|
|
220
|
+
Author: tsmishra
|
|
221
|
+
"""
|
|
222
|
+
result: List[ast.stmt] = []
|
|
223
|
+
for stmt in stmts:
|
|
224
|
+
result.append(stmt)
|
|
225
|
+
# Recurse into compound statement bodies to preserve order
|
|
226
|
+
for child_stmts in _child_stmt_lists(stmt):
|
|
227
|
+
result.extend(_walk_stmts_ordered(child_stmts))
|
|
228
|
+
return result
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _child_stmt_lists(stmt: ast.stmt) -> List[list]:
|
|
232
|
+
"""Return the child statement lists of a compound statement."""
|
|
233
|
+
out: List[list] = []
|
|
234
|
+
for attr in ("body", "orelse", "handlers", "finalbody"):
|
|
235
|
+
val = getattr(stmt, attr, None)
|
|
236
|
+
if val and isinstance(val, list):
|
|
237
|
+
# handlers are ExceptHandler nodes, not stmt lists
|
|
238
|
+
for item in val:
|
|
239
|
+
if isinstance(item, ast.ExceptHandler):
|
|
240
|
+
out.append(item.body)
|
|
241
|
+
elif isinstance(item, ast.stmt):
|
|
242
|
+
out.append([item])
|
|
243
|
+
elif isinstance(item, list):
|
|
244
|
+
out.append(item)
|
|
245
|
+
if all(isinstance(item, ast.stmt) for item in val):
|
|
246
|
+
out.append(val)
|
|
247
|
+
return out
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _propagate(
|
|
251
|
+
func_ast: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
252
|
+
tainted: Set[str],
|
|
253
|
+
source_lines: List[str],
|
|
254
|
+
file_path: str,
|
|
255
|
+
) -> List[TaintFinding]:
|
|
256
|
+
"""
|
|
257
|
+
Single-pass ordered walk of func_ast.body.
|
|
258
|
+
Mutates *tainted* in place (general taint propagation).
|
|
259
|
+
Also maintains *str_tainted*: variables that are tainted AND have passed
|
|
260
|
+
through a string construction operation — used for .execute() HIGH vs MEDIUM.
|
|
261
|
+
Returns TaintFinding list.
|
|
262
|
+
|
|
263
|
+
Author: tsmishra
|
|
264
|
+
"""
|
|
265
|
+
findings: List[TaintFinding] = []
|
|
266
|
+
str_tainted: Set[str] = set()
|
|
267
|
+
|
|
268
|
+
for stmt in _walk_stmts_ordered(func_ast.body):
|
|
269
|
+
# ------------------------------------------------------------------
|
|
270
|
+
# Assignment propagation
|
|
271
|
+
# ------------------------------------------------------------------
|
|
272
|
+
if isinstance(stmt, ast.Assign):
|
|
273
|
+
rhs_tainted = _expr_contains_tainted(stmt.value, tainted) or _is_source(stmt.value)
|
|
274
|
+
if rhs_tainted:
|
|
275
|
+
is_str_op = _is_string_construction(stmt.value, tainted)
|
|
276
|
+
for target in stmt.targets:
|
|
277
|
+
for n in ast.walk(target):
|
|
278
|
+
if isinstance(n, ast.Name):
|
|
279
|
+
tainted.add(n.id)
|
|
280
|
+
if is_str_op:
|
|
281
|
+
str_tainted.add(n.id)
|
|
282
|
+
# Sink inside RHS call: x = eval(tainted)
|
|
283
|
+
if isinstance(stmt.value, ast.Call):
|
|
284
|
+
_check_call(stmt.value, tainted, str_tainted, findings, source_lines, file_path)
|
|
285
|
+
|
|
286
|
+
elif isinstance(stmt, ast.AnnAssign):
|
|
287
|
+
if stmt.value:
|
|
288
|
+
rhs_tainted = _expr_contains_tainted(stmt.value, tainted) or _is_source(stmt.value)
|
|
289
|
+
if rhs_tainted and isinstance(stmt.target, ast.Name):
|
|
290
|
+
tainted.add(stmt.target.id)
|
|
291
|
+
if _is_string_construction(stmt.value, tainted):
|
|
292
|
+
str_tainted.add(stmt.target.id)
|
|
293
|
+
if isinstance(stmt.value, ast.Call):
|
|
294
|
+
_check_call(stmt.value, tainted, str_tainted, findings, source_lines, file_path)
|
|
295
|
+
|
|
296
|
+
elif isinstance(stmt, ast.AugAssign):
|
|
297
|
+
if _expr_contains_tainted(stmt.value, tainted):
|
|
298
|
+
if isinstance(stmt.target, ast.Name):
|
|
299
|
+
tainted.add(stmt.target.id)
|
|
300
|
+
if _is_string_construction(stmt.value, tainted):
|
|
301
|
+
str_tainted.add(stmt.target.id)
|
|
302
|
+
|
|
303
|
+
# ------------------------------------------------------------------
|
|
304
|
+
# Sink calls as expression statements
|
|
305
|
+
# ------------------------------------------------------------------
|
|
306
|
+
elif isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call):
|
|
307
|
+
_check_call(stmt.value, tainted, str_tainted, findings, source_lines, file_path)
|
|
308
|
+
|
|
309
|
+
# Sink inside return expr: return eval(tainted)
|
|
310
|
+
elif isinstance(stmt, ast.Return) and stmt.value:
|
|
311
|
+
if isinstance(stmt.value, ast.Call):
|
|
312
|
+
_check_call(stmt.value, tainted, str_tainted, findings, source_lines, file_path)
|
|
313
|
+
|
|
314
|
+
return findings
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _check_call(
|
|
318
|
+
call_node: ast.Call,
|
|
319
|
+
tainted: Set[str],
|
|
320
|
+
str_tainted: Set[str],
|
|
321
|
+
findings: List[TaintFinding],
|
|
322
|
+
source_lines: List[str],
|
|
323
|
+
file_path: str,
|
|
324
|
+
) -> None:
|
|
325
|
+
label = _sink_label(call_node)
|
|
326
|
+
if label is None:
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
is_execute = label == ".execute()"
|
|
330
|
+
|
|
331
|
+
if is_execute:
|
|
332
|
+
# SQL injection logic — only flag when string construction is provably observed.
|
|
333
|
+
#
|
|
334
|
+
# arg[0] is the query position. arg[1+] is the params slot (safe pattern).
|
|
335
|
+
#
|
|
336
|
+
# HIGH — string construction evidence exists:
|
|
337
|
+
# (a) arg[0] itself IS a string construction expression (inline f-string,
|
|
338
|
+
# concat, %-format, .format(), str()) that incorporates a tainted var.
|
|
339
|
+
# (b) arg[0] is a variable that was previously assigned via string
|
|
340
|
+
# construction from a tainted source (tracked in str_tainted).
|
|
341
|
+
#
|
|
342
|
+
# silent — no string construction evidence, OR taint only in arg[1+].
|
|
343
|
+
#
|
|
344
|
+
# Rationale: .execute() is a common method name (Chain of Responsibility,
|
|
345
|
+
# command patterns, task runners, workflow engines). Without type inference
|
|
346
|
+
# we cannot know if the receiver is a DB cursor. Requiring string construction
|
|
347
|
+
# evidence eliminates false positives from non-SQL .execute() calls while
|
|
348
|
+
# still catching the dangerous patterns that matter.
|
|
349
|
+
query_arg = call_node.args[0] if call_node.args else None
|
|
350
|
+
if query_arg is None:
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
arg0_names = _names_in_expr(query_arg)
|
|
354
|
+
|
|
355
|
+
# (a) inline string construction in arg[0] itself
|
|
356
|
+
inline_str_construction = _is_string_construction(query_arg, tainted)
|
|
357
|
+
# (b) arg[0] names a variable built via string construction earlier
|
|
358
|
+
prior_str_tainted = arg0_names & str_tainted
|
|
359
|
+
|
|
360
|
+
if inline_str_construction:
|
|
361
|
+
tainted_args = sorted(arg0_names & tainted)
|
|
362
|
+
elif prior_str_tainted:
|
|
363
|
+
tainted_args = sorted(prior_str_tainted)
|
|
364
|
+
else:
|
|
365
|
+
return # no string construction evidence — silent
|
|
366
|
+
|
|
367
|
+
confidence = "high"
|
|
368
|
+
|
|
369
|
+
else:
|
|
370
|
+
# All other sinks: flag any tainted arg in any position
|
|
371
|
+
tainted_args: List[str] = []
|
|
372
|
+
for arg in call_node.args:
|
|
373
|
+
hits = _names_in_expr(arg) & tainted
|
|
374
|
+
tainted_args.extend(sorted(hits))
|
|
375
|
+
for kw in call_node.keywords:
|
|
376
|
+
if kw.value:
|
|
377
|
+
hits = _names_in_expr(kw.value) & tainted
|
|
378
|
+
tainted_args.extend(sorted(hits))
|
|
379
|
+
if not tainted_args:
|
|
380
|
+
return
|
|
381
|
+
confidence = "high"
|
|
382
|
+
|
|
383
|
+
lineno = call_node.lineno
|
|
384
|
+
snippet = source_lines[lineno - 1].rstrip() if lineno <= len(source_lines) else ""
|
|
385
|
+
findings.append(TaintFinding(
|
|
386
|
+
file_path=file_path,
|
|
387
|
+
line_number=lineno,
|
|
388
|
+
sink=label,
|
|
389
|
+
source_vars=list(dict.fromkeys(tainted_args)),
|
|
390
|
+
code_snippet=snippet,
|
|
391
|
+
confidence=confidence,
|
|
392
|
+
))
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
# ---------------------------------------------------------------------------
|
|
396
|
+
# Public: analyse a single function AST node with optional extra taint seeds
|
|
397
|
+
# ---------------------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
def analyse_function_ast(
|
|
400
|
+
func_ast: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
401
|
+
source_lines: List[str],
|
|
402
|
+
file_path: str,
|
|
403
|
+
extra_tainted: Optional[Set[str]] = None,
|
|
404
|
+
) -> tuple[List[TaintFinding], Set[str]]:
|
|
405
|
+
"""
|
|
406
|
+
Analyse *func_ast* and return (findings, final_tainted_set).
|
|
407
|
+
|
|
408
|
+
extra_tainted: additional variable names to pre-seed as tainted.
|
|
409
|
+
The returned tainted set is used by the inter-procedural engine to
|
|
410
|
+
determine whether a `return` propagates taint to the caller.
|
|
411
|
+
|
|
412
|
+
Author: tsmishra
|
|
413
|
+
"""
|
|
414
|
+
args = func_ast.args
|
|
415
|
+
tainted: Set[str] = set()
|
|
416
|
+
for arg in args.args + args.posonlyargs + args.kwonlyargs:
|
|
417
|
+
tainted.add(arg.arg)
|
|
418
|
+
if args.vararg:
|
|
419
|
+
tainted.add(args.vararg.arg)
|
|
420
|
+
if args.kwarg:
|
|
421
|
+
tainted.add(args.kwarg.arg)
|
|
422
|
+
if extra_tainted:
|
|
423
|
+
tainted.update(extra_tainted)
|
|
424
|
+
|
|
425
|
+
findings = _propagate(func_ast, tainted, source_lines, file_path)
|
|
426
|
+
return findings, tainted
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def can_return_tainted(
|
|
430
|
+
func_ast: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
431
|
+
tainted: Set[str],
|
|
432
|
+
) -> bool:
|
|
433
|
+
"""
|
|
434
|
+
True if any `return <expr>` in the function references a tainted variable.
|
|
435
|
+
Used by the inter-procedural engine to propagate taint through return values.
|
|
436
|
+
|
|
437
|
+
Author: tsmishra
|
|
438
|
+
"""
|
|
439
|
+
for node in ast.walk(func_ast):
|
|
440
|
+
if isinstance(node, ast.Return) and node.value is not None:
|
|
441
|
+
if _expr_contains_tainted(node.value, tainted):
|
|
442
|
+
return True
|
|
443
|
+
return False
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
# ---------------------------------------------------------------------------
|
|
447
|
+
# Public: analyse a single file (intra-function only — legacy entry point)
|
|
448
|
+
# ---------------------------------------------------------------------------
|
|
449
|
+
|
|
450
|
+
def analyse_file(file_path: str) -> List[TaintFinding]:
|
|
451
|
+
"""
|
|
452
|
+
Parse *file_path* and return all intra-function taint findings.
|
|
453
|
+
|
|
454
|
+
Author: tsmishra
|
|
455
|
+
"""
|
|
456
|
+
try:
|
|
457
|
+
with open(file_path, encoding="utf-8", errors="replace") as fh:
|
|
458
|
+
source = fh.read()
|
|
459
|
+
except OSError:
|
|
460
|
+
return []
|
|
461
|
+
|
|
462
|
+
try:
|
|
463
|
+
tree = ast.parse(source, filename=file_path)
|
|
464
|
+
except SyntaxError:
|
|
465
|
+
return []
|
|
466
|
+
|
|
467
|
+
source_lines = source.splitlines()
|
|
468
|
+
findings: List[TaintFinding] = []
|
|
469
|
+
|
|
470
|
+
for node in ast.walk(tree):
|
|
471
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
472
|
+
fn_findings, _ = analyse_function_ast(node, source_lines, file_path)
|
|
473
|
+
findings.extend(fn_findings)
|
|
474
|
+
|
|
475
|
+
return findings
|