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,69 @@
|
|
|
1
|
+
"""Lexical visibility ranges for Java type declarations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Dict, Iterator, List, Tuple
|
|
6
|
+
|
|
7
|
+
from tree_sitter import Node
|
|
8
|
+
|
|
9
|
+
TYPE_DECLARATIONS = {
|
|
10
|
+
"class_declaration", "interface_declaration", "enum_declaration",
|
|
11
|
+
"record_declaration", "annotation_type_declaration",
|
|
12
|
+
}
|
|
13
|
+
# Body node types tree-sitter-java pairs with each `TYPE_DECLARATIONS` entry —
|
|
14
|
+
# *not* uniformly `class_body`: `interface_declaration` → `interface_body`,
|
|
15
|
+
# `enum_declaration` → `enum_body`, `annotation_type_declaration` →
|
|
16
|
+
# `annotation_type_body` (`class_declaration`/`record_declaration` are the
|
|
17
|
+
# only two that actually use `class_body`). Any CST walk that must stop at a
|
|
18
|
+
# nested type's boundary (a local/anonymous type's own scope, separately
|
|
19
|
+
# analyzed) needs *all four*, not just `class_body` — a caller that checks
|
|
20
|
+
# only `class_body` will walk straight through a local enum/interface/
|
|
21
|
+
# `@interface`'s body and misattribute its statements to the enclosing
|
|
22
|
+
# method (PROBLEMS.md P4.6).
|
|
23
|
+
TYPE_BODIES = {"class_body", "interface_body", "enum_body", "annotation_type_body"}
|
|
24
|
+
TypeScopes = Dict[str, List[Tuple[int, int]]]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _walk(node: Node) -> Iterator[Node]:
|
|
28
|
+
yield node
|
|
29
|
+
for child in node.children:
|
|
30
|
+
yield from _walk(child)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def declaration_scope(node: Node, root: Node) -> Tuple[int, int]:
|
|
34
|
+
"""Lexical visibility range for a type declaration `node`: from its own
|
|
35
|
+
start (a local class isn't visible before its declaration point) to the
|
|
36
|
+
end of whichever encloses it — the nearest `block` for a local class, the
|
|
37
|
+
nearest enclosing type's body for a member class, or `root` for a
|
|
38
|
+
top-level class. Shared by `declared_type_scopes` (below) and
|
|
39
|
+
`type_provenance.local_supertypes_of`, so a class's variable-declaration
|
|
40
|
+
scope and its extends/implements scope always agree (PROBLEMS.md P4.6
|
|
41
|
+
F-02: two local classes of the same simple name in sibling methods each
|
|
42
|
+
get their own scope instead of colliding in a single name-keyed table)."""
|
|
43
|
+
parent = node.parent
|
|
44
|
+
while parent is not None and parent is not root:
|
|
45
|
+
if parent.type == "block":
|
|
46
|
+
return (node.start_byte, parent.end_byte)
|
|
47
|
+
if parent.type in TYPE_DECLARATIONS:
|
|
48
|
+
body = parent.child_by_field_name("body")
|
|
49
|
+
scope = body if body is not None else parent
|
|
50
|
+
return (scope.start_byte, scope.end_byte)
|
|
51
|
+
parent = parent.parent
|
|
52
|
+
return (root.start_byte, root.end_byte)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def declared_type_scopes(root: Node, source: bytes) -> TypeScopes:
|
|
56
|
+
scopes: TypeScopes = {}
|
|
57
|
+
for node in _walk(root):
|
|
58
|
+
if node.type not in TYPE_DECLARATIONS:
|
|
59
|
+
continue
|
|
60
|
+
name_node = node.child_by_field_name("name")
|
|
61
|
+
if name_node is None:
|
|
62
|
+
continue
|
|
63
|
+
name = source[name_node.start_byte:name_node.end_byte].decode("utf-8")
|
|
64
|
+
scopes.setdefault(name, []).append(declaration_scope(node, root))
|
|
65
|
+
return scopes
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def type_declared_at(scopes: TypeScopes, name: str, at_byte: int) -> bool:
|
|
69
|
+
return any(start <= at_byte < end for start, end in scopes.get(name, ()))
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
"""Java intra-function taint mechanics (P4.6, PROBLEMS.md).
|
|
2
|
+
|
|
3
|
+
`JavaTaintStrategy` is `detectors.taint.TaintStrategy`'s Java implementation
|
|
4
|
+
— the CST-coupled half of P2.19's taint tracking, mirroring `lang/python/
|
|
5
|
+
taint.py::PythonTaintStrategy`'s shape but driven by Java's own source/sink
|
|
6
|
+
tables (`lang/java/cfg.py`) and grammar.
|
|
7
|
+
|
|
8
|
+
The one genuinely Java-specific addition beyond a grammar port: a formal
|
|
9
|
+
parameter carrying a request-binding annotation (`@RequestParam` et al.,
|
|
10
|
+
`CFGConfig.taint_source_param_annotations`) is tainted *unconditionally* at
|
|
11
|
+
function entry — not merely "could be tainted, caller decides" the way an
|
|
12
|
+
ordinary parameter is. This is what makes codegraph-java-deep-dive.md
|
|
13
|
+
§10.5's example work: `UserController.search(@RequestParam String q)` is
|
|
14
|
+
itself the taint's origin, with no caller needed to seed it.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
20
|
+
|
|
21
|
+
from tree_sitter import Node
|
|
22
|
+
|
|
23
|
+
from adl_drift.detectors.taint import TaintPath, TaintStrategy, _OutCall
|
|
24
|
+
from adl_drift.lang.base import CFGConfig
|
|
25
|
+
from adl_drift.lang.java.cfg import (
|
|
26
|
+
SINK_CONSTRUCTOR_ARGUMENTS,
|
|
27
|
+
SINK_CONSTRUCTOR_OWNERS,
|
|
28
|
+
SINK_METHOD_ARGUMENTS,
|
|
29
|
+
SINK_METHOD_EXTERNAL_SUBTYPE_FALLBACKS,
|
|
30
|
+
SINK_METHOD_OWNERS,
|
|
31
|
+
SinkArgumentPolicy,
|
|
32
|
+
TAINT_SOURCE_PARAM_ANNOTATION_OWNERS,
|
|
33
|
+
_annotation_owner_verdict,
|
|
34
|
+
_annotations_of,
|
|
35
|
+
_collect_uses,
|
|
36
|
+
_constructed_receiver_type,
|
|
37
|
+
_enclosing_type_name,
|
|
38
|
+
_erased_type_name,
|
|
39
|
+
_method_parameters,
|
|
40
|
+
_owner_provenance,
|
|
41
|
+
_receiver_type_candidates,
|
|
42
|
+
_source_match_for,
|
|
43
|
+
_text,
|
|
44
|
+
)
|
|
45
|
+
from adl_drift.lang.java.scopes import TYPE_BODIES
|
|
46
|
+
from adl_drift.lang.java.type_provenance import JavaTypeContext, build_type_context
|
|
47
|
+
from adl_drift.utils.types import SourcePosition
|
|
48
|
+
|
|
49
|
+
__all__ = ["JavaTaintStrategy", "default_java_taint_strategy"]
|
|
50
|
+
|
|
51
|
+
# Node types marking a *different* function/type's own scope — walks over a
|
|
52
|
+
# method body must not descend past one of these (mirrors `lang.java.
|
|
53
|
+
# resolution.constants._NESTED_SCOPE_BOUNDARY`'s intent, but correct for
|
|
54
|
+
# every nested-type body tree-sitter-java produces, not just `class_body`;
|
|
55
|
+
# see `scopes.TYPE_BODIES`'s docstring — PROBLEMS.md P4.6).
|
|
56
|
+
_NESTED_SCOPE_BOUNDARY = (
|
|
57
|
+
"method_declaration", "constructor_declaration",
|
|
58
|
+
"compact_constructor_declaration", "lambda_expression", *TYPE_BODIES,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _pos(node: Node) -> SourcePosition:
|
|
63
|
+
return SourcePosition(
|
|
64
|
+
line=node.start_point[0] + 1,
|
|
65
|
+
column=node.start_point[1],
|
|
66
|
+
end_line=node.end_point[0] + 1,
|
|
67
|
+
end_column=node.end_point[1],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _ids_in(node: Optional[Node], source: bytes) -> Set[str]:
|
|
72
|
+
"""Identifier *reads* under *node* — reuses `lang.java.cfg`'s use-walker
|
|
73
|
+
(already skips method/field names, type positions, annotations, and
|
|
74
|
+
lambda bodies) rather than duplicating that logic."""
|
|
75
|
+
out: Set[str] = set()
|
|
76
|
+
if node is None:
|
|
77
|
+
return out
|
|
78
|
+
events: List = []
|
|
79
|
+
_collect_uses(node, events)
|
|
80
|
+
return {e.name for e in events}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _this_field_name(node: Optional[Node], source: bytes) -> str:
|
|
84
|
+
if node is None or node.type != "field_access":
|
|
85
|
+
return ""
|
|
86
|
+
obj = node.child_by_field_name("object")
|
|
87
|
+
field = node.child_by_field_name("field")
|
|
88
|
+
if obj is None or field is None or obj.type != "this":
|
|
89
|
+
return ""
|
|
90
|
+
return _text(field, source)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _taint_ids_in(node: Optional[Node], source: bytes) -> Set[str]:
|
|
94
|
+
"""Binding reads for taint propagation, including `this.field` aliases."""
|
|
95
|
+
out = _ids_in(node, source)
|
|
96
|
+
if node is None:
|
|
97
|
+
return out
|
|
98
|
+
stack = [node]
|
|
99
|
+
while stack:
|
|
100
|
+
cur = stack.pop()
|
|
101
|
+
name = _this_field_name(cur, source)
|
|
102
|
+
if name:
|
|
103
|
+
out.add(name)
|
|
104
|
+
continue
|
|
105
|
+
stack.extend(cur.children)
|
|
106
|
+
return out
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _target_names(node: Optional[Node], source: bytes) -> List[str]:
|
|
110
|
+
if node is None:
|
|
111
|
+
return []
|
|
112
|
+
if node.type == "identifier":
|
|
113
|
+
name = _text(node, source)
|
|
114
|
+
return [name] if name else []
|
|
115
|
+
name = _this_field_name(node, source)
|
|
116
|
+
return [name] if name else []
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _positional_args(call: Node) -> List[Node]:
|
|
120
|
+
args = call.child_by_field_name("arguments")
|
|
121
|
+
if args is None:
|
|
122
|
+
return []
|
|
123
|
+
return [c for c in args.children if c.is_named]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _sink_args(call: Node, policy: SinkArgumentPolicy) -> List[Node]:
|
|
127
|
+
if policy == "receiver":
|
|
128
|
+
# Zero-argument sink (`in.readObject()`) — the node to check for
|
|
129
|
+
# taint is the receiver expression itself, not a positional
|
|
130
|
+
# argument. Reusing the same "list of nodes checked for taint"
|
|
131
|
+
# contract as the argument-index policies below lets the caller's
|
|
132
|
+
# generic sink-taint check (`analyze_one`) handle this case with no
|
|
133
|
+
# extra branching.
|
|
134
|
+
obj = call.child_by_field_name("object")
|
|
135
|
+
return [obj] if obj is not None else []
|
|
136
|
+
pos_args = _positional_args(call)
|
|
137
|
+
if policy == "all":
|
|
138
|
+
return pos_args
|
|
139
|
+
return [pos_args[idx] for idx in policy if 0 <= idx < len(pos_args)]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _param_names(func_node: Node, source: bytes) -> List[str]:
|
|
143
|
+
params_node = _method_parameters(func_node)
|
|
144
|
+
if params_node is None:
|
|
145
|
+
return []
|
|
146
|
+
names: List[str] = []
|
|
147
|
+
for child in params_node.children:
|
|
148
|
+
if child.type not in ("formal_parameter", "spread_parameter"):
|
|
149
|
+
continue
|
|
150
|
+
name_node = child.child_by_field_name("name")
|
|
151
|
+
if name_node is not None:
|
|
152
|
+
names.append(_text(name_node, source))
|
|
153
|
+
return names
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _annotation_seeds(
|
|
157
|
+
func_node: Node, source: bytes, config: CFGConfig, ctx: Optional[JavaTypeContext],
|
|
158
|
+
) -> Dict[str, Tuple[str, bool]]:
|
|
159
|
+
"""`{param_name: (source_label, confirmed)}` for every formal parameter
|
|
160
|
+
carrying a request-binding annotation (`@RequestParam` et al.).
|
|
161
|
+
`confirmed` is `False` for an annotation that couldn't be positively
|
|
162
|
+
matched against `TAINT_SOURCE_PARAM_ANNOTATION_OWNERS` via import (F-04,
|
|
163
|
+
PROBLEMS.md P4.6) — still seeded, at reduced downstream confidence,
|
|
164
|
+
unless the annotation is *provably* a different, same-named one (a
|
|
165
|
+
local `@interface` declaration, or a conflicting import), in which case
|
|
166
|
+
it's skipped entirely."""
|
|
167
|
+
params_node = _method_parameters(func_node)
|
|
168
|
+
if params_node is None:
|
|
169
|
+
return {}
|
|
170
|
+
seeds: Dict[str, Tuple[str, bool]] = {}
|
|
171
|
+
for child in params_node.children:
|
|
172
|
+
if child.type not in ("formal_parameter", "spread_parameter"):
|
|
173
|
+
continue
|
|
174
|
+
name_node = child.child_by_field_name("name")
|
|
175
|
+
if name_node is None:
|
|
176
|
+
continue
|
|
177
|
+
for raw_name, at_byte in _annotations_of(child, source):
|
|
178
|
+
simple = raw_name.rsplit(".", 1)[-1]
|
|
179
|
+
label = config.taint_source_param_annotations.get(simple)
|
|
180
|
+
if not label:
|
|
181
|
+
continue
|
|
182
|
+
verdict = _annotation_owner_verdict(
|
|
183
|
+
raw_name, at_byte, TAINT_SOURCE_PARAM_ANNOTATION_OWNERS.get(simple), ctx,
|
|
184
|
+
)
|
|
185
|
+
if verdict == "rejected":
|
|
186
|
+
continue
|
|
187
|
+
seeds[_text(name_node, source)] = (label, verdict == "confirmed")
|
|
188
|
+
break
|
|
189
|
+
return seeds
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class JavaTaintStrategy(TaintStrategy):
|
|
193
|
+
"""Java's `TaintStrategy` (P4.6)."""
|
|
194
|
+
|
|
195
|
+
def __init__(self, config: CFGConfig) -> None:
|
|
196
|
+
super().__init__(config)
|
|
197
|
+
# Per-file import/declared-type provenance (F-04, PROBLEMS.md P4.6),
|
|
198
|
+
# computed once per file and reused for every function `analyze_one`
|
|
199
|
+
# is called on within it — safe to cache for the lifetime of this
|
|
200
|
+
# strategy instance, which is itself scoped to one `CallGraphBuilder`
|
|
201
|
+
# (one scan snapshot; see `CallGraphBuilder._get_taint_strategy`).
|
|
202
|
+
self._type_ctx_cache: Dict[str, JavaTypeContext] = {}
|
|
203
|
+
|
|
204
|
+
def param_names(self, func_node: Node, source: bytes) -> List[str]:
|
|
205
|
+
return _param_names(func_node, source)
|
|
206
|
+
|
|
207
|
+
def _type_context(self, func_node: Node, source: bytes, file_path: str) -> JavaTypeContext:
|
|
208
|
+
ctx = self._type_ctx_cache.get(file_path)
|
|
209
|
+
if ctx is None:
|
|
210
|
+
root = func_node
|
|
211
|
+
while root.parent is not None:
|
|
212
|
+
root = root.parent
|
|
213
|
+
ctx = build_type_context(root, source)
|
|
214
|
+
self._type_ctx_cache[file_path] = ctx
|
|
215
|
+
return ctx
|
|
216
|
+
|
|
217
|
+
def _source_match(
|
|
218
|
+
self, node: Optional[Node], source: bytes, ctx: Optional[JavaTypeContext],
|
|
219
|
+
) -> Optional[Tuple[str, bool]]:
|
|
220
|
+
return _source_match_for(node, self.config, source, ctx)
|
|
221
|
+
|
|
222
|
+
def _sink_info(
|
|
223
|
+
self, call: Node, is_new: bool, source: bytes, ctx: Optional[JavaTypeContext],
|
|
224
|
+
) -> Optional[Tuple[str, str, List[Node], bool]]:
|
|
225
|
+
"""Returns ``(callable_text, category, pos_args, confirmed)`` — *all*
|
|
226
|
+
positional args, not just the first: Java varargs sinks (`new
|
|
227
|
+
ProcessBuilder(String...)`, `.command(String...)`) spread the
|
|
228
|
+
attacker-controlled data across multiple sibling argument nodes,
|
|
229
|
+
unlike Python's sinks (`eval`, `os.system`) which are always
|
|
230
|
+
single-argument. ``confirmed`` is `False` when the sink name matched
|
|
231
|
+
but its declaring type couldn't be positively confirmed via imports
|
|
232
|
+
(F-04, PROBLEMS.md P4.6) — a business method named `execute`/
|
|
233
|
+
`invoke`/`exec` on a project-owned, unrelated class returns `None`
|
|
234
|
+
entirely (its type is *confirmed* to be something else), not a
|
|
235
|
+
low-confidence match."""
|
|
236
|
+
if is_new:
|
|
237
|
+
type_node = call.child_by_field_name("type")
|
|
238
|
+
if type_node is None:
|
|
239
|
+
return None
|
|
240
|
+
simple = _erased_type_name(_text(type_node, source))
|
|
241
|
+
sink_key = simple if simple in self.config.sink_constructors else None
|
|
242
|
+
if sink_key is None:
|
|
243
|
+
# Fully-qualified constructed type (`new java.lang.
|
|
244
|
+
# ProcessBuilder(...)`), not just the short `ProcessBuilder`
|
|
245
|
+
# form `_erased_type_name` above catches — same FQN gap as
|
|
246
|
+
# `_source_match_for`'s (PROBLEMS.md P4.6 F-04): `simple`
|
|
247
|
+
# above never equals the short `sink_constructors` key for
|
|
248
|
+
# an FQN-spelled type, so it fell through unmatched. Only
|
|
249
|
+
# trusted when `simple` equals the *entire* owner FQN, not a
|
|
250
|
+
# suffix of it, mirroring the source-side fix.
|
|
251
|
+
for cand_key, owner_fqns in SINK_CONSTRUCTOR_OWNERS.items():
|
|
252
|
+
if cand_key in self.config.sink_constructors and simple in owner_fqns:
|
|
253
|
+
sink_key = cand_key
|
|
254
|
+
break
|
|
255
|
+
category = self.config.sink_constructors.get(sink_key) if sink_key else None
|
|
256
|
+
if category is None:
|
|
257
|
+
return None
|
|
258
|
+
pos_args = _sink_args(call, SINK_CONSTRUCTOR_ARGUMENTS.get(sink_key, "all"))
|
|
259
|
+
if not pos_args:
|
|
260
|
+
return None
|
|
261
|
+
verdict = _owner_provenance(
|
|
262
|
+
(simple,), call.start_byte, SINK_CONSTRUCTOR_OWNERS.get(sink_key), ctx,
|
|
263
|
+
)
|
|
264
|
+
if verdict == "rejected":
|
|
265
|
+
return None
|
|
266
|
+
return f"new {sink_key}", category, pos_args, verdict == "confirmed"
|
|
267
|
+
|
|
268
|
+
name_node = call.child_by_field_name("name")
|
|
269
|
+
if name_node is None:
|
|
270
|
+
return None
|
|
271
|
+
method_name = _text(name_node, source)
|
|
272
|
+
category = self.config.sink_methods.get(method_name)
|
|
273
|
+
if category is None:
|
|
274
|
+
return None
|
|
275
|
+
pos_args = _sink_args(call, SINK_METHOD_ARGUMENTS.get(method_name, "all"))
|
|
276
|
+
if not pos_args:
|
|
277
|
+
return None
|
|
278
|
+
obj = call.child_by_field_name("object")
|
|
279
|
+
receiver = _text(obj, source) if obj is not None else ""
|
|
280
|
+
verdict = _owner_provenance(
|
|
281
|
+
_receiver_type_candidates(
|
|
282
|
+
receiver, ctx, call.start_byte, _constructed_receiver_type(obj, source),
|
|
283
|
+
_enclosing_type_name(call, source) if obj is None else "",
|
|
284
|
+
),
|
|
285
|
+
call.start_byte, SINK_METHOD_OWNERS.get(method_name), ctx,
|
|
286
|
+
allow_external_subtype_fallback=method_name in SINK_METHOD_EXTERNAL_SUBTYPE_FALLBACKS,
|
|
287
|
+
)
|
|
288
|
+
if verdict == "rejected":
|
|
289
|
+
return None
|
|
290
|
+
callable_text = f"{receiver}.{method_name}" if obj is not None else method_name
|
|
291
|
+
return callable_text, category, pos_args, verdict == "confirmed"
|
|
292
|
+
|
|
293
|
+
@staticmethod
|
|
294
|
+
def _collect_call_sites(body: Node) -> List[Tuple[Node, bool]]:
|
|
295
|
+
"""`(call_node, is_object_creation)` for every `method_invocation`/
|
|
296
|
+
`object_creation_expression` in `body`, not descending into a nested
|
|
297
|
+
method/constructor/class body or a lambda (each has its own,
|
|
298
|
+
separately-analyzed scope)."""
|
|
299
|
+
out: List[Tuple[Node, bool]] = []
|
|
300
|
+
stack = [body]
|
|
301
|
+
while stack:
|
|
302
|
+
node = stack.pop()
|
|
303
|
+
if node.type == "method_invocation":
|
|
304
|
+
out.append((node, False))
|
|
305
|
+
elif node.type == "object_creation_expression":
|
|
306
|
+
out.append((node, True))
|
|
307
|
+
if node is not body and node.type in _NESTED_SCOPE_BOUNDARY:
|
|
308
|
+
continue
|
|
309
|
+
stack.extend(node.children)
|
|
310
|
+
return out
|
|
311
|
+
|
|
312
|
+
def analyze_one(
|
|
313
|
+
self,
|
|
314
|
+
func_node: Node,
|
|
315
|
+
source: bytes,
|
|
316
|
+
function_id: str,
|
|
317
|
+
file_path: str,
|
|
318
|
+
seed_params: Set[str],
|
|
319
|
+
) -> Tuple[Set[str], List[TaintPath], List[_OutCall]]:
|
|
320
|
+
body = func_node.child_by_field_name("body")
|
|
321
|
+
if body is None:
|
|
322
|
+
return set(), [], []
|
|
323
|
+
|
|
324
|
+
ctx = self._type_context(func_node, source, file_path)
|
|
325
|
+
|
|
326
|
+
tainted: Set[str] = set(seed_params)
|
|
327
|
+
source_of: Dict[str, str] = {p: "parameter" for p in seed_params}
|
|
328
|
+
# Whether each binding's taint origin was FQN-confirmed (F-04,
|
|
329
|
+
# PROBLEMS.md P4.6) rather than an unresolved simple-name fallback —
|
|
330
|
+
# cross-function seeds have no source/sink match of their own to
|
|
331
|
+
# confirm, so they default to confirmed (their confidence is already
|
|
332
|
+
# governed by the existing cross-function 0.7-vs-0.85 split below).
|
|
333
|
+
confirmed_of: Dict[str, bool] = {p: True for p in seed_params}
|
|
334
|
+
|
|
335
|
+
# Request-binding annotations are an unconditional source, regardless
|
|
336
|
+
# of caller — see module docstring.
|
|
337
|
+
for name, (label, confirmed) in _annotation_seeds(func_node, source, self.config, ctx).items():
|
|
338
|
+
tainted.add(name)
|
|
339
|
+
source_of[name] = label
|
|
340
|
+
confirmed_of[name] = confirmed
|
|
341
|
+
|
|
342
|
+
# ── collect assignment records ──────────────────────────────────
|
|
343
|
+
# `(names, rhs_ids, direct_label, direct_confirmed)` — `direct_label`
|
|
344
|
+
# is set only when the RHS itself is a recognized source expression.
|
|
345
|
+
assigns: List[Tuple[List[str], Set[str], Optional[str], bool]] = []
|
|
346
|
+
|
|
347
|
+
def _record(target_node: Optional[Node], value: Optional[Node]) -> None:
|
|
348
|
+
names = _target_names(target_node, source)
|
|
349
|
+
if not names or value is None:
|
|
350
|
+
return
|
|
351
|
+
match = self._source_match(value, source, ctx)
|
|
352
|
+
label, confirmed = match if match else (None, True)
|
|
353
|
+
assigns.append((names, _taint_ids_in(value, source), label, confirmed))
|
|
354
|
+
|
|
355
|
+
stack = [body]
|
|
356
|
+
while stack:
|
|
357
|
+
node = stack.pop()
|
|
358
|
+
t = node.type
|
|
359
|
+
if node is not body and t in _NESTED_SCOPE_BOUNDARY:
|
|
360
|
+
continue
|
|
361
|
+
if t == "local_variable_declaration":
|
|
362
|
+
for i, child in enumerate(node.children):
|
|
363
|
+
if node.field_name_for_child(i) != "declarator":
|
|
364
|
+
continue
|
|
365
|
+
_record(child.child_by_field_name("name"), child.child_by_field_name("value"))
|
|
366
|
+
elif t == "assignment_expression":
|
|
367
|
+
lhs = node.child_by_field_name("left")
|
|
368
|
+
op = node.child_by_field_name("operator")
|
|
369
|
+
rhs = node.child_by_field_name("right")
|
|
370
|
+
op_text = _text(op, source) if op is not None else "="
|
|
371
|
+
names = _target_names(lhs, source)
|
|
372
|
+
if names and rhs is not None:
|
|
373
|
+
if op_text == "=":
|
|
374
|
+
_record(lhs, rhs)
|
|
375
|
+
else:
|
|
376
|
+
rhs_ids = _taint_ids_in(rhs, source) | set(names)
|
|
377
|
+
match = self._source_match(rhs, source, ctx)
|
|
378
|
+
label, confirmed = match if match else (None, True)
|
|
379
|
+
assigns.append((names, rhs_ids, label, confirmed))
|
|
380
|
+
elif t == "enhanced_for_statement":
|
|
381
|
+
_record(node.child_by_field_name("name"), node.child_by_field_name("value"))
|
|
382
|
+
elif t == "resource":
|
|
383
|
+
_record(node.child_by_field_name("name"), node.child_by_field_name("value"))
|
|
384
|
+
stack.extend(node.children)
|
|
385
|
+
|
|
386
|
+
# ── propagate to fixpoint ─────────────────────────────────────────
|
|
387
|
+
changed = True
|
|
388
|
+
while changed:
|
|
389
|
+
changed = False
|
|
390
|
+
for names, rhs_ids, lbl, lbl_confirmed in assigns:
|
|
391
|
+
tainted_rhs = lbl or any(i in tainted for i in rhs_ids)
|
|
392
|
+
if not tainted_rhs:
|
|
393
|
+
continue
|
|
394
|
+
if lbl:
|
|
395
|
+
label, confirmed = lbl, lbl_confirmed
|
|
396
|
+
else:
|
|
397
|
+
hit = next((i for i in rhs_ids if i in tainted), None)
|
|
398
|
+
label = source_of.get(hit)
|
|
399
|
+
confirmed = confirmed_of.get(hit, True)
|
|
400
|
+
for n in names:
|
|
401
|
+
if n not in tainted:
|
|
402
|
+
tainted.add(n)
|
|
403
|
+
source_of[n] = label or "tainted"
|
|
404
|
+
confirmed_of[n] = confirmed
|
|
405
|
+
changed = True
|
|
406
|
+
|
|
407
|
+
# ── sink detection ────────────────────────────────────────────────
|
|
408
|
+
paths: List[TaintPath] = []
|
|
409
|
+
seen_sites: Set[Tuple[int, int]] = set()
|
|
410
|
+
for call, is_new in self._collect_call_sites(body):
|
|
411
|
+
info = self._sink_info(call, is_new, source, ctx)
|
|
412
|
+
if info is None:
|
|
413
|
+
continue
|
|
414
|
+
callable_text, category, arg_nodes, sink_confirmed = info
|
|
415
|
+
site = _pos(call)
|
|
416
|
+
site_key = (site.line, site.column)
|
|
417
|
+
|
|
418
|
+
direct: Optional[str] = None
|
|
419
|
+
direct_confirmed = True
|
|
420
|
+
binding: Optional[str] = None
|
|
421
|
+
for arg_node in arg_nodes:
|
|
422
|
+
match = self._source_match(arg_node, source, ctx)
|
|
423
|
+
if match:
|
|
424
|
+
direct, direct_confirmed = match
|
|
425
|
+
break
|
|
426
|
+
arg_ids = _taint_ids_in(arg_node, source)
|
|
427
|
+
hit = next((i for i in arg_ids if i in tainted), None)
|
|
428
|
+
if hit is not None:
|
|
429
|
+
binding = hit
|
|
430
|
+
break
|
|
431
|
+
if direct is None and binding is None:
|
|
432
|
+
continue
|
|
433
|
+
|
|
434
|
+
if direct:
|
|
435
|
+
label = direct
|
|
436
|
+
cross = False
|
|
437
|
+
source_confirmed = direct_confirmed
|
|
438
|
+
else:
|
|
439
|
+
label = source_of.get(binding, "tainted")
|
|
440
|
+
cross = label == "parameter"
|
|
441
|
+
source_confirmed = confirmed_of.get(binding, True)
|
|
442
|
+
|
|
443
|
+
if site_key in seen_sites:
|
|
444
|
+
continue
|
|
445
|
+
seen_sites.add(site_key)
|
|
446
|
+
|
|
447
|
+
chain = [label]
|
|
448
|
+
if binding:
|
|
449
|
+
chain.append(binding)
|
|
450
|
+
chain.append(callable_text)
|
|
451
|
+
# Confidence combines the existing same-function/cross-function
|
|
452
|
+
# split with whether *both* the source and sink API were
|
|
453
|
+
# FQN-confirmed (F-04, PROBLEMS.md P4.6) — an unresolved simple-
|
|
454
|
+
# name match on either side is still reported (recall), just at
|
|
455
|
+
# a markedly lower confidence than a fully-confirmed one.
|
|
456
|
+
confirmed = source_confirmed and sink_confirmed
|
|
457
|
+
if confirmed:
|
|
458
|
+
confidence = 0.7 if cross else 0.85
|
|
459
|
+
else:
|
|
460
|
+
confidence = 0.35 if cross else 0.5
|
|
461
|
+
paths.append(TaintPath(
|
|
462
|
+
function_id=function_id,
|
|
463
|
+
file_path=file_path,
|
|
464
|
+
sink=callable_text,
|
|
465
|
+
sink_category=category,
|
|
466
|
+
source_label=label,
|
|
467
|
+
binding_name=binding,
|
|
468
|
+
sink_site=site,
|
|
469
|
+
chain=chain,
|
|
470
|
+
confidence=confidence,
|
|
471
|
+
cross_function=cross,
|
|
472
|
+
))
|
|
473
|
+
|
|
474
|
+
# ── outgoing tainted calls (for cross-function propagation) ────────
|
|
475
|
+
out_calls: List[_OutCall] = []
|
|
476
|
+
for call, is_new in self._collect_call_sites(body):
|
|
477
|
+
if is_new:
|
|
478
|
+
continue # constructor targets aren't looked up by-name (P4.4 precedent)
|
|
479
|
+
name_node = call.child_by_field_name("name")
|
|
480
|
+
if name_node is None:
|
|
481
|
+
continue
|
|
482
|
+
callee = _text(name_node, source)
|
|
483
|
+
if not callee:
|
|
484
|
+
continue
|
|
485
|
+
pos_t: Set[int] = set()
|
|
486
|
+
for idx, arg in enumerate(_positional_args(call)):
|
|
487
|
+
if self._source_match(arg, source, ctx) or (_taint_ids_in(arg, source) & tainted):
|
|
488
|
+
pos_t.add(idx)
|
|
489
|
+
if pos_t:
|
|
490
|
+
out_calls.append(_OutCall(
|
|
491
|
+
callee, pos_t, set(),
|
|
492
|
+
site_line=call.start_point[0] + 1,
|
|
493
|
+
site_column=call.start_point[1],
|
|
494
|
+
))
|
|
495
|
+
|
|
496
|
+
return tainted, paths, out_calls
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def default_java_taint_strategy(config: CFGConfig) -> JavaTaintStrategy:
|
|
500
|
+
return JavaTaintStrategy(config)
|