adl-toolkit 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL_TOOLKIT.md +163 -0
- package/README.md +269 -2
- package/adl_drift/__init__.py +6 -0
- package/adl_drift/cli/__init__.py +1 -0
- package/adl_drift/cli/commands.py +993 -0
- package/adl_drift/cli/main.py +744 -0
- package/adl_drift/cortex/__init__.py +1 -0
- package/adl_drift/cortex/embeddings.py +96 -0
- package/adl_drift/cortex/memory.py +1749 -0
- package/adl_drift/cortex/patterns.py +89 -0
- package/adl_drift/cortex/telemetry.py +192 -0
- package/adl_drift/detectors/__init__.py +1 -0
- package/adl_drift/detectors/async_patterns.py +115 -0
- package/adl_drift/detectors/base.py +121 -0
- package/adl_drift/detectors/config.py +122 -0
- package/adl_drift/detectors/data_access.py +128 -0
- package/adl_drift/detectors/decorators.py +122 -0
- package/adl_drift/detectors/dynamic_patterns.py +260 -0
- package/adl_drift/detectors/errors.py +134 -0
- package/adl_drift/detectors/imports.py +132 -0
- package/adl_drift/detectors/logging_patterns.py +108 -0
- package/adl_drift/detectors/routes.py +257 -0
- package/adl_drift/detectors/security.py +236 -0
- package/adl_drift/detectors/taint.py +406 -0
- package/adl_drift/detectors/tests.py +95 -0
- package/adl_drift/detectors/type_conventions.py +81 -0
- package/adl_drift/graph/__init__.py +1 -0
- package/adl_drift/graph/builder.py +2670 -0
- package/adl_drift/graph/cfg.py +461 -0
- package/adl_drift/graph/cluster.py +40 -0
- package/adl_drift/graph/edges.py +86 -0
- package/adl_drift/graph/node.py +90 -0
- package/adl_drift/harness/__init__.py +10 -0
- package/adl_drift/harness/setup.py +832 -0
- package/adl_drift/harness/skills/drift-antigravity.md +19 -0
- package/adl_drift/harness/skills/drift-generic.md +19 -0
- package/adl_drift/harness/skills/drift.md +25 -0
- package/adl_drift/harness/watch.py +84 -0
- package/adl_drift/lang/__init__.py +8 -0
- package/adl_drift/lang/base.py +464 -0
- package/adl_drift/lang/java/__init__.py +6 -0
- package/adl_drift/lang/java/backend.py +97 -0
- package/adl_drift/lang/java/cfg.py +1424 -0
- package/adl_drift/lang/java/config.py +707 -0
- package/adl_drift/lang/java/detectors/__init__.py +29 -0
- package/adl_drift/lang/java/detectors/additional.py +445 -0
- package/adl_drift/lang/java/detectors/base.py +154 -0
- package/adl_drift/lang/java/detectors/data_access.py +148 -0
- package/adl_drift/lang/java/detectors/imports.py +53 -0
- package/adl_drift/lang/java/detectors/routes.py +159 -0
- package/adl_drift/lang/java/detectors/security.py +386 -0
- package/adl_drift/lang/java/detectors/tests.py +74 -0
- package/adl_drift/lang/java/extractor.py +582 -0
- package/adl_drift/lang/java/ids.py +99 -0
- package/adl_drift/lang/java/parser.py +49 -0
- package/adl_drift/lang/java/resolution/__init__.py +1 -0
- package/adl_drift/lang/java/resolution/constants.py +1619 -0
- package/adl_drift/lang/java/resolution/constructors.py +278 -0
- package/adl_drift/lang/java/resolution/context.py +305 -0
- package/adl_drift/lang/java/resolution/dispatch.py +540 -0
- package/adl_drift/lang/java/resolution/functional.py +1318 -0
- package/adl_drift/lang/java/resolution/generated.py +95 -0
- package/adl_drift/lang/java/resolution/model.py +153 -0
- package/adl_drift/lang/java/resolution/orchestration.py +866 -0
- package/adl_drift/lang/java/resolution/overloads.py +1275 -0
- package/adl_drift/lang/java/resolution/scopes.py +1111 -0
- package/adl_drift/lang/java/resolution/types.py +2947 -0
- package/adl_drift/lang/java/resolver.py +1401 -0
- package/adl_drift/lang/java/scopes.py +69 -0
- package/adl_drift/lang/java/taint.py +500 -0
- package/adl_drift/lang/java/type_provenance.py +342 -0
- package/adl_drift/lang/python/__init__.py +8 -0
- package/adl_drift/lang/python/backend.py +94 -0
- package/adl_drift/lang/python/cfg.py +537 -0
- package/adl_drift/lang/python/extractor.py +454 -0
- package/adl_drift/lang/python/parser.py +41 -0
- package/adl_drift/lang/python/resolver.py +1789 -0
- package/adl_drift/lang/python/taint.py +365 -0
- package/adl_drift/lang/registry.py +117 -0
- package/adl_drift/parser/__init__.py +1 -0
- package/adl_drift/parser/cache.py +38 -0
- package/adl_drift/parser/core.py +33 -0
- package/adl_drift/parser/extractor.py +17 -0
- package/adl_drift/parser/literal_eval.py +326 -0
- package/adl_drift/scanner/__init__.py +1 -0
- package/adl_drift/scanner/discovery.py +338 -0
- package/adl_drift/scanner/incremental.py +261 -0
- package/adl_drift/scanner/parallel.py +224 -0
- package/adl_drift/server/__init__.py +1 -0
- package/adl_drift/server/handlers.py +2849 -0
- package/adl_drift/server/mcp_server.py +127 -0
- package/adl_drift/server/mcp_stdio.py +335 -0
- package/adl_drift/server/protocol.py +71 -0
- package/adl_drift/server/static/index.html +696 -0
- package/adl_drift/server/web.py +350 -0
- package/adl_drift/utils/__init__.py +1 -0
- package/adl_drift/utils/config.py +77 -0
- package/adl_drift/utils/fswalk.py +52 -0
- package/adl_drift/utils/types.py +345 -0
- package/bin/adl-drift.mjs +35 -0
- package/bin/adl-toolkit.mjs +488 -0
- package/package.json +29 -4
- package/pyproject.toml +49 -0
- package/scripts/install_toolkit.py +3584 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
"""Python CFG construction (P2.18, ported unchanged onto the P4.6 seam).
|
|
2
|
+
|
|
3
|
+
`PythonCFGBuilder` is the exact pre-P4.6 `graph.cfg.CFGBuilder` dispatch
|
|
4
|
+
logic (statement-kind CST walk, literal/taint-source detection on RHS text),
|
|
5
|
+
now a `graph.cfg.CFGBuilderBase` subclass reading its taint-source list from
|
|
6
|
+
`self.config.taint_sources` (a `CFGConfig`) instead of a hardcoded module
|
|
7
|
+
constant. No behavioral change for Python — `default_python_cfg_config()`
|
|
8
|
+
below packages the original `TAINT_SOURCES` list into that config, so every
|
|
9
|
+
existing call site (which never passed a config) sees byte-identical output.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Dict, List, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
from tree_sitter import Node
|
|
17
|
+
|
|
18
|
+
from adl_drift.graph.cfg import (
|
|
19
|
+
CFGBlock,
|
|
20
|
+
CFGBuilderBase,
|
|
21
|
+
CFGDef,
|
|
22
|
+
_is_literal,
|
|
23
|
+
_node_pos,
|
|
24
|
+
_text,
|
|
25
|
+
)
|
|
26
|
+
from adl_drift.lang.base import CFGConfig
|
|
27
|
+
|
|
28
|
+
__all__ = ["TAINT_SOURCES", "PythonCFGBuilder", "default_python_cfg_config"]
|
|
29
|
+
|
|
30
|
+
# ─── taint source prefixes (P2.18/P2.19 original table) ──────────────────────
|
|
31
|
+
|
|
32
|
+
TAINT_SOURCES: Tuple[str, ...] = (
|
|
33
|
+
# HTTP request objects (Django / Flask / ASGI / Starlette …)
|
|
34
|
+
"request.GET", "request.POST", "request.body", "request.json",
|
|
35
|
+
"request.form", "request.args", "request.data", "request.values",
|
|
36
|
+
"request.FILES", "request.COOKIES",
|
|
37
|
+
"request.get_json", "request.get_data",
|
|
38
|
+
# WSGI / ASGI environ
|
|
39
|
+
"environ.get", "scope.get",
|
|
40
|
+
# OS env-vars
|
|
41
|
+
"os.environ.get", "os.getenv",
|
|
42
|
+
# Other user inputs
|
|
43
|
+
"sys.argv", "input(",
|
|
44
|
+
# DB results (can contain user data that should not be trusted)
|
|
45
|
+
"cursor.fetchone", "cursor.fetchall", "cursor.fetchmany",
|
|
46
|
+
# Path / file args coming in from outside
|
|
47
|
+
"open(",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# ─── statement types that terminate a block (no normal fall-through) ─────────
|
|
51
|
+
|
|
52
|
+
_TERMINATOR_TYPES: frozenset = frozenset({
|
|
53
|
+
"return_statement", "raise_statement",
|
|
54
|
+
"break_statement", "continue_statement",
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
# ─── statement types with no control-flow split (fall straight through) ──────
|
|
58
|
+
# Note: expression_statement is NOT here — it's handled separately in
|
|
59
|
+
# _process_stmt because tree-sitter Python wraps assignment / augmented_assignment
|
|
60
|
+
# / call nodes inside expression_statement.
|
|
61
|
+
|
|
62
|
+
_SIMPLE_STMT_TYPES: frozenset = frozenset({
|
|
63
|
+
"delete_statement",
|
|
64
|
+
"import_statement", "import_from_statement",
|
|
65
|
+
"assert_statement",
|
|
66
|
+
"global_statement", "nonlocal_statement",
|
|
67
|
+
"pass_statement",
|
|
68
|
+
"print_statement", # Python 2 guard
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ─── helper functions (Python-CST-coupled) ────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
def _real_children(node: Node) -> List[Node]:
|
|
75
|
+
"""Non-extra, non-punctuation children."""
|
|
76
|
+
return [c for c in node.children
|
|
77
|
+
if not c.is_extra and c.type not in (",", ";", ":", "\n", "(", ")")]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _lhs_names(node: Node, source: bytes) -> List[str]:
|
|
81
|
+
"""Extract identifier names from an assignment LHS (handles tuples, lists)."""
|
|
82
|
+
if node.type == "identifier":
|
|
83
|
+
return [_text(node, source)]
|
|
84
|
+
names: List[str] = []
|
|
85
|
+
for child in node.children:
|
|
86
|
+
if child.type not in (",", "(", ")", "[", "]", "*"):
|
|
87
|
+
names.extend(_lhs_names(child, source))
|
|
88
|
+
return names
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _uses_of(node: Node) -> List:
|
|
92
|
+
"""Convenience wrapper: collect uses from *node* and return as a new list."""
|
|
93
|
+
from adl_drift.graph.cfg import CFGUse
|
|
94
|
+
out: List = []
|
|
95
|
+
_collect_uses(node, out)
|
|
96
|
+
return out
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _collect_uses(node: Node, uses: List, _visited: Optional[set] = None) -> None:
|
|
100
|
+
"""Walk *node* and append identifier reads to *uses*.
|
|
101
|
+
|
|
102
|
+
Skips:
|
|
103
|
+
- LHS of assignments (handled separately)
|
|
104
|
+
- Keyword argument names (``kwarg=val``)
|
|
105
|
+
- Attribute name parts (``obj.attr`` → only ``obj`` is a read)
|
|
106
|
+
- Type annotation nodes
|
|
107
|
+
- Function / class definition names
|
|
108
|
+
"""
|
|
109
|
+
from adl_drift.graph.cfg import CFGUse
|
|
110
|
+
|
|
111
|
+
if _visited is None:
|
|
112
|
+
_visited = set()
|
|
113
|
+
nid = id(node)
|
|
114
|
+
if nid in _visited:
|
|
115
|
+
return
|
|
116
|
+
_visited.add(nid)
|
|
117
|
+
|
|
118
|
+
t = node.type
|
|
119
|
+
|
|
120
|
+
if t == "assignment":
|
|
121
|
+
rhs = node.child_by_field_name("right")
|
|
122
|
+
if rhs:
|
|
123
|
+
_collect_uses(rhs, uses, _visited)
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
if t == "augmented_assignment":
|
|
127
|
+
lhs = node.child_by_field_name("left")
|
|
128
|
+
rhs = node.child_by_field_name("right")
|
|
129
|
+
if lhs and lhs.type == "identifier":
|
|
130
|
+
uses.append(CFGUse(_text(lhs, b""), _node_pos(lhs)))
|
|
131
|
+
if rhs:
|
|
132
|
+
_collect_uses(rhs, uses, _visited)
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
if t == "attribute":
|
|
136
|
+
# obj.attr — only obj is a read; attr is a field name
|
|
137
|
+
obj = node.child_by_field_name("object")
|
|
138
|
+
if obj:
|
|
139
|
+
_collect_uses(obj, uses, _visited)
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
if t == "keyword_argument":
|
|
143
|
+
val = node.child_by_field_name("value")
|
|
144
|
+
if val:
|
|
145
|
+
_collect_uses(val, uses, _visited)
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
if t in ("function_definition", "class_definition", "lambda"):
|
|
149
|
+
# Don't descend into nested defs (separate function analysis)
|
|
150
|
+
name_node = node.child_by_field_name("name")
|
|
151
|
+
if name_node and name_node.type == "identifier":
|
|
152
|
+
uses.append(CFGUse(_text(name_node, b""), _node_pos(name_node)))
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
if t == "type" or t == "type_annotation":
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
if t == "identifier":
|
|
159
|
+
name = node.text.decode("utf-8", errors="replace") if node.text else ""
|
|
160
|
+
if name:
|
|
161
|
+
uses.append(CFGUse(name, _node_pos(node)))
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
for child in node.children:
|
|
165
|
+
_collect_uses(child, uses, _visited)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _collect_param_names(params_node: Node, source: bytes) -> List[str]:
|
|
169
|
+
"""Extract all parameter identifier names from a ``parameters`` CST node."""
|
|
170
|
+
names: List[str] = []
|
|
171
|
+
for child in params_node.children:
|
|
172
|
+
ct = child.type
|
|
173
|
+
if ct == "identifier":
|
|
174
|
+
names.append(_text(child, source))
|
|
175
|
+
elif ct in ("typed_parameter", "default_parameter", "typed_default_parameter",
|
|
176
|
+
"list_splat_pattern", "dictionary_splat_pattern"):
|
|
177
|
+
inner = child.child_by_field_name("name") or (
|
|
178
|
+
child.children[0] if child.children else None
|
|
179
|
+
)
|
|
180
|
+
if inner and inner.type == "identifier":
|
|
181
|
+
names.append(_text(inner, source))
|
|
182
|
+
return names
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ─── CFG builder ──────────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
class PythonCFGBuilder(CFGBuilderBase):
|
|
188
|
+
"""Converts a ``function_definition`` CST node into a basic-block CFG."""
|
|
189
|
+
|
|
190
|
+
def build(
|
|
191
|
+
self, func_node: Node, source: bytes
|
|
192
|
+
) -> Tuple[int, int, Dict[int, CFGBlock], List[CFGDef]]:
|
|
193
|
+
self._reset()
|
|
194
|
+
|
|
195
|
+
entry = self._new_block()
|
|
196
|
+
exit_b = self._new_block()
|
|
197
|
+
|
|
198
|
+
# Parameters are defs in the entry block
|
|
199
|
+
params = func_node.child_by_field_name("parameters")
|
|
200
|
+
if params:
|
|
201
|
+
for name in _collect_param_names(params, source):
|
|
202
|
+
d = self._new_def(name, _node_pos(params), False, None)
|
|
203
|
+
entry.events.append(d)
|
|
204
|
+
|
|
205
|
+
body = func_node.child_by_field_name("body")
|
|
206
|
+
if body:
|
|
207
|
+
tail = self._process_stmts(_real_children(body), entry, exit_b, source)
|
|
208
|
+
if tail is not None:
|
|
209
|
+
self._link(tail, exit_b)
|
|
210
|
+
else:
|
|
211
|
+
self._link(entry, exit_b)
|
|
212
|
+
|
|
213
|
+
return entry.id, exit_b.id, self._blocks, self._all_defs
|
|
214
|
+
|
|
215
|
+
# ── internal helpers ──────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
def _taint_of(self, node: Node, source: bytes) -> Optional[str]:
|
|
218
|
+
"""Return a taint-source label if node's text starts with a known source."""
|
|
219
|
+
text = _text(node, source).strip()
|
|
220
|
+
for src in self.config.taint_sources:
|
|
221
|
+
if text.startswith(src):
|
|
222
|
+
return src
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
def _process_stmts(
|
|
226
|
+
self,
|
|
227
|
+
stmts: List[Node],
|
|
228
|
+
cur: CFGBlock,
|
|
229
|
+
exit_b: CFGBlock,
|
|
230
|
+
source: bytes,
|
|
231
|
+
) -> Optional[CFGBlock]:
|
|
232
|
+
for stmt in stmts:
|
|
233
|
+
if cur is None:
|
|
234
|
+
return None
|
|
235
|
+
cur = self._process_stmt(stmt, cur, exit_b, source)
|
|
236
|
+
return cur
|
|
237
|
+
|
|
238
|
+
def _process_stmt(
|
|
239
|
+
self,
|
|
240
|
+
stmt: Node,
|
|
241
|
+
cur: CFGBlock,
|
|
242
|
+
exit_b: CFGBlock,
|
|
243
|
+
source: bytes,
|
|
244
|
+
) -> Optional[CFGBlock]:
|
|
245
|
+
t = stmt.type
|
|
246
|
+
|
|
247
|
+
# ── tree-sitter Python wraps ALL simple stmts in expression_statement ──
|
|
248
|
+
# Unwrap and re-dispatch so assignment/augmented_assignment defs are
|
|
249
|
+
# captured, and calls are treated as expressions (uses only).
|
|
250
|
+
if t == "expression_statement":
|
|
251
|
+
real_children = [c for c in stmt.children
|
|
252
|
+
if not c.is_extra and c.type not in ("\n", "comment")]
|
|
253
|
+
if len(real_children) == 1:
|
|
254
|
+
return self._process_stmt(real_children[0], cur, exit_b, source)
|
|
255
|
+
# Multiple children (e.g. type-annotated assignment) — collect uses
|
|
256
|
+
cur.events.extend(_uses_of(stmt))
|
|
257
|
+
return cur
|
|
258
|
+
|
|
259
|
+
# ── simple assignment ─────────────────────────────────────────────────
|
|
260
|
+
if t == "assignment":
|
|
261
|
+
rhs = stmt.child_by_field_name("right")
|
|
262
|
+
lhs = stmt.child_by_field_name("left")
|
|
263
|
+
is_lit = _is_literal(rhs) if rhs else False
|
|
264
|
+
taint = self._taint_of(rhs, source) if rhs else None
|
|
265
|
+
if rhs:
|
|
266
|
+
cur.events.extend(_uses_of(rhs))
|
|
267
|
+
if lhs:
|
|
268
|
+
for name in _lhs_names(lhs, source):
|
|
269
|
+
if name:
|
|
270
|
+
cur.events.append(
|
|
271
|
+
self._new_def(name, _node_pos(lhs), is_lit, taint)
|
|
272
|
+
)
|
|
273
|
+
return cur
|
|
274
|
+
|
|
275
|
+
# ── augmented assignment ──────────────────────────────────────────────
|
|
276
|
+
if t == "augmented_assignment":
|
|
277
|
+
lhs = stmt.child_by_field_name("left")
|
|
278
|
+
rhs = stmt.child_by_field_name("right")
|
|
279
|
+
if rhs:
|
|
280
|
+
cur.events.extend(_uses_of(rhs))
|
|
281
|
+
if lhs and lhs.type == "identifier":
|
|
282
|
+
name = _text(lhs, source)
|
|
283
|
+
if name:
|
|
284
|
+
from adl_drift.graph.cfg import CFGUse
|
|
285
|
+
cur.events.append(CFGUse(name, _node_pos(lhs)))
|
|
286
|
+
cur.events.append(self._new_def(name, _node_pos(lhs), False, None))
|
|
287
|
+
return cur
|
|
288
|
+
|
|
289
|
+
# ── named expression (walrus) ─────────────────────────────────────────
|
|
290
|
+
if t == "named_expression":
|
|
291
|
+
name_node = stmt.child_by_field_name("name")
|
|
292
|
+
val = stmt.child_by_field_name("value")
|
|
293
|
+
if val:
|
|
294
|
+
cur.events.extend(_uses_of(val))
|
|
295
|
+
if name_node and name_node.type == "identifier":
|
|
296
|
+
name = _text(name_node, source)
|
|
297
|
+
is_lit = _is_literal(val) if val else False
|
|
298
|
+
taint = self._taint_of(val, source) if val else None
|
|
299
|
+
cur.events.append(self._new_def(name, _node_pos(name_node), is_lit, taint))
|
|
300
|
+
return cur
|
|
301
|
+
|
|
302
|
+
# ── terminators ───────────────────────────────────────────────────────
|
|
303
|
+
if t in _TERMINATOR_TYPES:
|
|
304
|
+
cur.events.extend(_uses_of(stmt))
|
|
305
|
+
self._link(cur, exit_b)
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
# ── simple pass-through ───────────────────────────────────────────────
|
|
309
|
+
if t in _SIMPLE_STMT_TYPES:
|
|
310
|
+
cur.events.extend(_uses_of(stmt))
|
|
311
|
+
return cur
|
|
312
|
+
|
|
313
|
+
# ── if / elif / else ──────────────────────────────────────────────────
|
|
314
|
+
if t == "if_statement":
|
|
315
|
+
return self._process_if(stmt, cur, exit_b, source)
|
|
316
|
+
|
|
317
|
+
# ── for / while loops ─────────────────────────────────────────────────
|
|
318
|
+
if t in ("for_statement", "while_statement"):
|
|
319
|
+
return self._process_loop(stmt, cur, exit_b, source)
|
|
320
|
+
|
|
321
|
+
# ── try / except / finally ────────────────────────────────────────────
|
|
322
|
+
if t == "try_statement":
|
|
323
|
+
return self._process_try(stmt, cur, exit_b, source)
|
|
324
|
+
|
|
325
|
+
# ── with statement ────────────────────────────────────────────────────
|
|
326
|
+
if t == "with_statement":
|
|
327
|
+
return self._process_with(stmt, cur, exit_b, source)
|
|
328
|
+
|
|
329
|
+
# ── decorated / nested definitions ────────────────────────────────────
|
|
330
|
+
if t in ("function_definition", "async_function_definition",
|
|
331
|
+
"class_definition", "decorated_definition"):
|
|
332
|
+
inner = stmt
|
|
333
|
+
if t == "decorated_definition":
|
|
334
|
+
inner = stmt.child_by_field_name("definition") or stmt
|
|
335
|
+
name_node = inner.child_by_field_name("name")
|
|
336
|
+
if name_node and name_node.type == "identifier":
|
|
337
|
+
name = _text(name_node, source)
|
|
338
|
+
cur.events.append(self._new_def(name, _node_pos(name_node), False, None))
|
|
339
|
+
return cur
|
|
340
|
+
|
|
341
|
+
# ── async wrapper ─────────────────────────────────────────────────────
|
|
342
|
+
if t == "async_statement":
|
|
343
|
+
for child in stmt.children:
|
|
344
|
+
if child.type not in ("async",):
|
|
345
|
+
cur = self._process_stmt(child, cur, exit_b, source)
|
|
346
|
+
if cur is None:
|
|
347
|
+
return None
|
|
348
|
+
return cur
|
|
349
|
+
|
|
350
|
+
# ── fallback: conservative — collect all identifiers as uses ──────────
|
|
351
|
+
cur.events.extend(_uses_of(stmt))
|
|
352
|
+
return cur
|
|
353
|
+
|
|
354
|
+
# ── compound statement helpers ────────────────────────────────────────────
|
|
355
|
+
|
|
356
|
+
def _process_if(
|
|
357
|
+
self, node: Node, cur: CFGBlock, exit_b: CFGBlock, source: bytes
|
|
358
|
+
) -> Optional[CFGBlock]:
|
|
359
|
+
cond = node.child_by_field_name("condition")
|
|
360
|
+
if cond:
|
|
361
|
+
cur.events.extend(_uses_of(cond))
|
|
362
|
+
|
|
363
|
+
merge = self._new_block()
|
|
364
|
+
|
|
365
|
+
# then-branch
|
|
366
|
+
body = node.child_by_field_name("consequence")
|
|
367
|
+
if body:
|
|
368
|
+
then_b = self._new_block()
|
|
369
|
+
self._link(cur, then_b)
|
|
370
|
+
tail = self._process_stmts(_real_children(body), then_b, exit_b, source)
|
|
371
|
+
if tail is not None:
|
|
372
|
+
self._link(tail, merge)
|
|
373
|
+
else:
|
|
374
|
+
self._link(cur, merge)
|
|
375
|
+
|
|
376
|
+
# else / elif (alternative field)
|
|
377
|
+
alt = node.child_by_field_name("alternative")
|
|
378
|
+
if alt is None:
|
|
379
|
+
self._link(cur, merge) # no else → fall through
|
|
380
|
+
else:
|
|
381
|
+
if alt.type == "else_clause":
|
|
382
|
+
alt_b = self._new_block()
|
|
383
|
+
self._link(cur, alt_b)
|
|
384
|
+
body_of_else = alt.child_by_field_name("body")
|
|
385
|
+
children = _real_children(body_of_else) if body_of_else else _real_children(alt)
|
|
386
|
+
tail = self._process_stmts(children, alt_b, exit_b, source)
|
|
387
|
+
if tail is not None:
|
|
388
|
+
self._link(tail, merge)
|
|
389
|
+
elif alt.type == "elif_clause":
|
|
390
|
+
# Treat as nested if
|
|
391
|
+
alt_b = self._new_block()
|
|
392
|
+
self._link(cur, alt_b)
|
|
393
|
+
tail = self._process_stmt(alt, alt_b, exit_b, source)
|
|
394
|
+
if tail is not None:
|
|
395
|
+
self._link(tail, merge)
|
|
396
|
+
else:
|
|
397
|
+
self._link(cur, merge)
|
|
398
|
+
|
|
399
|
+
return merge
|
|
400
|
+
|
|
401
|
+
def _process_loop(
|
|
402
|
+
self, node: Node, cur: CFGBlock, exit_b: CFGBlock, source: bytes
|
|
403
|
+
) -> Optional[CFGBlock]:
|
|
404
|
+
header = self._new_block()
|
|
405
|
+
body_b = self._new_block()
|
|
406
|
+
after = self._new_block()
|
|
407
|
+
|
|
408
|
+
self._link(cur, header)
|
|
409
|
+
|
|
410
|
+
# Iterator expression (for) or condition (while)
|
|
411
|
+
iter_node = (node.child_by_field_name("right") or
|
|
412
|
+
node.child_by_field_name("condition"))
|
|
413
|
+
if iter_node:
|
|
414
|
+
header.events.extend(_uses_of(iter_node))
|
|
415
|
+
|
|
416
|
+
# Loop variable defs (for-loop only)
|
|
417
|
+
left = node.child_by_field_name("left")
|
|
418
|
+
if left:
|
|
419
|
+
for name in _lhs_names(left, source):
|
|
420
|
+
if name:
|
|
421
|
+
header.defs.append(self._new_def(name, _node_pos(left), False, None))
|
|
422
|
+
|
|
423
|
+
# header → body (iterations exist) or after (zero iterations)
|
|
424
|
+
self._link(header, body_b)
|
|
425
|
+
self._link(header, after)
|
|
426
|
+
|
|
427
|
+
body = node.child_by_field_name("body")
|
|
428
|
+
if body:
|
|
429
|
+
tail = self._process_stmts(_real_children(body), body_b, exit_b, source)
|
|
430
|
+
if tail is not None:
|
|
431
|
+
self._link(tail, header) # back-edge
|
|
432
|
+
|
|
433
|
+
# else-clause (loop not broken early)
|
|
434
|
+
alt = node.child_by_field_name("alternative")
|
|
435
|
+
if alt:
|
|
436
|
+
else_b = self._new_block()
|
|
437
|
+
self._link(after, else_b)
|
|
438
|
+
alt_body = alt.child_by_field_name("body")
|
|
439
|
+
children = _real_children(alt_body) if alt_body else _real_children(alt)
|
|
440
|
+
tail = self._process_stmts(children, else_b, exit_b, source)
|
|
441
|
+
if tail:
|
|
442
|
+
after = tail # merge point shifts to end of else
|
|
443
|
+
|
|
444
|
+
return after
|
|
445
|
+
|
|
446
|
+
def _process_try(
|
|
447
|
+
self, node: Node, cur: CFGBlock, exit_b: CFGBlock, source: bytes
|
|
448
|
+
) -> Optional[CFGBlock]:
|
|
449
|
+
merge = self._new_block()
|
|
450
|
+
|
|
451
|
+
# try body
|
|
452
|
+
body = node.child_by_field_name("body")
|
|
453
|
+
if body:
|
|
454
|
+
try_b = self._new_block()
|
|
455
|
+
self._link(cur, try_b)
|
|
456
|
+
tail = self._process_stmts(_real_children(body), try_b, exit_b, source)
|
|
457
|
+
if tail:
|
|
458
|
+
self._link(tail, merge)
|
|
459
|
+
else:
|
|
460
|
+
self._link(cur, merge)
|
|
461
|
+
|
|
462
|
+
# except handlers — conservative: all are reachable from try entry
|
|
463
|
+
for child in node.children:
|
|
464
|
+
if child.type in ("except_clause", "except_group_clause"):
|
|
465
|
+
exc_b = self._new_block()
|
|
466
|
+
self._link(cur, exc_b)
|
|
467
|
+
# "as <name>" exception variable def
|
|
468
|
+
saw_as = False
|
|
469
|
+
for gc in child.children:
|
|
470
|
+
if saw_as and gc.type == "identifier":
|
|
471
|
+
exc_b.defs.append(
|
|
472
|
+
self._new_def(_text(gc, source), _node_pos(gc), False, None)
|
|
473
|
+
)
|
|
474
|
+
saw_as = False
|
|
475
|
+
elif gc.type == "as":
|
|
476
|
+
saw_as = True
|
|
477
|
+
exc_body = child.child_by_field_name("body")
|
|
478
|
+
if exc_body:
|
|
479
|
+
tail = self._process_stmts(
|
|
480
|
+
_real_children(exc_body), exc_b, exit_b, source
|
|
481
|
+
)
|
|
482
|
+
if tail:
|
|
483
|
+
self._link(tail, merge)
|
|
484
|
+
else:
|
|
485
|
+
self._link(exc_b, merge)
|
|
486
|
+
|
|
487
|
+
elif child.type == "finally_clause":
|
|
488
|
+
fin_body = child.child_by_field_name("body")
|
|
489
|
+
if fin_body:
|
|
490
|
+
fin_b = self._new_block()
|
|
491
|
+
self._link(merge, fin_b)
|
|
492
|
+
tail = self._process_stmts(
|
|
493
|
+
_real_children(fin_body), fin_b, exit_b, source
|
|
494
|
+
)
|
|
495
|
+
if tail:
|
|
496
|
+
merge = tail
|
|
497
|
+
|
|
498
|
+
return merge
|
|
499
|
+
|
|
500
|
+
def _process_with(
|
|
501
|
+
self, node: Node, cur: CFGBlock, exit_b: CFGBlock, source: bytes
|
|
502
|
+
) -> Optional[CFGBlock]:
|
|
503
|
+
# Process each with_item: value uses + alias defs
|
|
504
|
+
for child in node.children:
|
|
505
|
+
if child.type == "with_item":
|
|
506
|
+
val = child.child_by_field_name("value")
|
|
507
|
+
alias = child.child_by_field_name("alias")
|
|
508
|
+
if val:
|
|
509
|
+
cur.events.extend(_uses_of(val))
|
|
510
|
+
taint = self._taint_of(val, source)
|
|
511
|
+
if alias and alias.type == "identifier":
|
|
512
|
+
name = _text(alias, source)
|
|
513
|
+
cur.events.append(
|
|
514
|
+
self._new_def(name, _node_pos(alias), False, taint)
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
body = node.child_by_field_name("body")
|
|
518
|
+
if body:
|
|
519
|
+
cur = self._process_stmts(_real_children(body), cur, exit_b, source)
|
|
520
|
+
return cur
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def default_python_cfg_config() -> CFGConfig:
|
|
524
|
+
"""The real Python `CFGConfig` — returned by `PythonBackend.cfg_config()`
|
|
525
|
+
and defaulted to by `graph/cfg.py::analyze_function`/`analyze_file` when
|
|
526
|
+
no config is passed (every pre-P4.6 call site)."""
|
|
527
|
+
from adl_drift.lang.python.taint import (
|
|
528
|
+
TAINT_SINKS, SINK_METHODS, default_python_taint_strategy,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
return CFGConfig(
|
|
532
|
+
taint_sources=TAINT_SOURCES,
|
|
533
|
+
taint_sinks=dict(TAINT_SINKS),
|
|
534
|
+
sink_methods=dict(SINK_METHODS),
|
|
535
|
+
cfg_builder_factory=lambda config: PythonCFGBuilder(config),
|
|
536
|
+
taint_strategy_factory=lambda config: default_python_taint_strategy(config),
|
|
537
|
+
)
|