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.
Files changed (104) hide show
  1. package/INSTALL_TOOLKIT.md +163 -0
  2. package/README.md +269 -2
  3. package/adl_drift/__init__.py +6 -0
  4. package/adl_drift/cli/__init__.py +1 -0
  5. package/adl_drift/cli/commands.py +993 -0
  6. package/adl_drift/cli/main.py +744 -0
  7. package/adl_drift/cortex/__init__.py +1 -0
  8. package/adl_drift/cortex/embeddings.py +96 -0
  9. package/adl_drift/cortex/memory.py +1749 -0
  10. package/adl_drift/cortex/patterns.py +89 -0
  11. package/adl_drift/cortex/telemetry.py +192 -0
  12. package/adl_drift/detectors/__init__.py +1 -0
  13. package/adl_drift/detectors/async_patterns.py +115 -0
  14. package/adl_drift/detectors/base.py +121 -0
  15. package/adl_drift/detectors/config.py +122 -0
  16. package/adl_drift/detectors/data_access.py +128 -0
  17. package/adl_drift/detectors/decorators.py +122 -0
  18. package/adl_drift/detectors/dynamic_patterns.py +260 -0
  19. package/adl_drift/detectors/errors.py +134 -0
  20. package/adl_drift/detectors/imports.py +132 -0
  21. package/adl_drift/detectors/logging_patterns.py +108 -0
  22. package/adl_drift/detectors/routes.py +257 -0
  23. package/adl_drift/detectors/security.py +236 -0
  24. package/adl_drift/detectors/taint.py +406 -0
  25. package/adl_drift/detectors/tests.py +95 -0
  26. package/adl_drift/detectors/type_conventions.py +81 -0
  27. package/adl_drift/graph/__init__.py +1 -0
  28. package/adl_drift/graph/builder.py +2670 -0
  29. package/adl_drift/graph/cfg.py +461 -0
  30. package/adl_drift/graph/cluster.py +40 -0
  31. package/adl_drift/graph/edges.py +86 -0
  32. package/adl_drift/graph/node.py +90 -0
  33. package/adl_drift/harness/__init__.py +10 -0
  34. package/adl_drift/harness/setup.py +832 -0
  35. package/adl_drift/harness/skills/drift-antigravity.md +19 -0
  36. package/adl_drift/harness/skills/drift-generic.md +19 -0
  37. package/adl_drift/harness/skills/drift.md +25 -0
  38. package/adl_drift/harness/watch.py +84 -0
  39. package/adl_drift/lang/__init__.py +8 -0
  40. package/adl_drift/lang/base.py +464 -0
  41. package/adl_drift/lang/java/__init__.py +6 -0
  42. package/adl_drift/lang/java/backend.py +97 -0
  43. package/adl_drift/lang/java/cfg.py +1424 -0
  44. package/adl_drift/lang/java/config.py +707 -0
  45. package/adl_drift/lang/java/detectors/__init__.py +29 -0
  46. package/adl_drift/lang/java/detectors/additional.py +445 -0
  47. package/adl_drift/lang/java/detectors/base.py +154 -0
  48. package/adl_drift/lang/java/detectors/data_access.py +148 -0
  49. package/adl_drift/lang/java/detectors/imports.py +53 -0
  50. package/adl_drift/lang/java/detectors/routes.py +159 -0
  51. package/adl_drift/lang/java/detectors/security.py +386 -0
  52. package/adl_drift/lang/java/detectors/tests.py +74 -0
  53. package/adl_drift/lang/java/extractor.py +582 -0
  54. package/adl_drift/lang/java/ids.py +99 -0
  55. package/adl_drift/lang/java/parser.py +49 -0
  56. package/adl_drift/lang/java/resolution/__init__.py +1 -0
  57. package/adl_drift/lang/java/resolution/constants.py +1619 -0
  58. package/adl_drift/lang/java/resolution/constructors.py +278 -0
  59. package/adl_drift/lang/java/resolution/context.py +305 -0
  60. package/adl_drift/lang/java/resolution/dispatch.py +540 -0
  61. package/adl_drift/lang/java/resolution/functional.py +1318 -0
  62. package/adl_drift/lang/java/resolution/generated.py +95 -0
  63. package/adl_drift/lang/java/resolution/model.py +153 -0
  64. package/adl_drift/lang/java/resolution/orchestration.py +866 -0
  65. package/adl_drift/lang/java/resolution/overloads.py +1275 -0
  66. package/adl_drift/lang/java/resolution/scopes.py +1111 -0
  67. package/adl_drift/lang/java/resolution/types.py +2947 -0
  68. package/adl_drift/lang/java/resolver.py +1401 -0
  69. package/adl_drift/lang/java/scopes.py +69 -0
  70. package/adl_drift/lang/java/taint.py +500 -0
  71. package/adl_drift/lang/java/type_provenance.py +342 -0
  72. package/adl_drift/lang/python/__init__.py +8 -0
  73. package/adl_drift/lang/python/backend.py +94 -0
  74. package/adl_drift/lang/python/cfg.py +537 -0
  75. package/adl_drift/lang/python/extractor.py +454 -0
  76. package/adl_drift/lang/python/parser.py +41 -0
  77. package/adl_drift/lang/python/resolver.py +1789 -0
  78. package/adl_drift/lang/python/taint.py +365 -0
  79. package/adl_drift/lang/registry.py +117 -0
  80. package/adl_drift/parser/__init__.py +1 -0
  81. package/adl_drift/parser/cache.py +38 -0
  82. package/adl_drift/parser/core.py +33 -0
  83. package/adl_drift/parser/extractor.py +17 -0
  84. package/adl_drift/parser/literal_eval.py +326 -0
  85. package/adl_drift/scanner/__init__.py +1 -0
  86. package/adl_drift/scanner/discovery.py +338 -0
  87. package/adl_drift/scanner/incremental.py +261 -0
  88. package/adl_drift/scanner/parallel.py +224 -0
  89. package/adl_drift/server/__init__.py +1 -0
  90. package/adl_drift/server/handlers.py +2849 -0
  91. package/adl_drift/server/mcp_server.py +127 -0
  92. package/adl_drift/server/mcp_stdio.py +335 -0
  93. package/adl_drift/server/protocol.py +71 -0
  94. package/adl_drift/server/static/index.html +696 -0
  95. package/adl_drift/server/web.py +350 -0
  96. package/adl_drift/utils/__init__.py +1 -0
  97. package/adl_drift/utils/config.py +77 -0
  98. package/adl_drift/utils/fswalk.py +52 -0
  99. package/adl_drift/utils/types.py +345 -0
  100. package/bin/adl-drift.mjs +35 -0
  101. package/bin/adl-toolkit.mjs +488 -0
  102. package/package.json +29 -4
  103. package/pyproject.toml +49 -0
  104. 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
+ )