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,29 @@
|
|
|
1
|
+
"""Java-specific registry detectors (P4.3)."""
|
|
2
|
+
|
|
3
|
+
from adl_drift.lang.java.detectors.additional import (
|
|
4
|
+
JavaAnnotationDetector,
|
|
5
|
+
JavaAsyncDetector,
|
|
6
|
+
JavaConfigDetector,
|
|
7
|
+
JavaDynamicDetector,
|
|
8
|
+
JavaLoggingDetector,
|
|
9
|
+
JavaTypeDetector,
|
|
10
|
+
)
|
|
11
|
+
from adl_drift.lang.java.detectors.data_access import JavaDataAccessDetector
|
|
12
|
+
from adl_drift.lang.java.detectors.imports import JavaImportDetector
|
|
13
|
+
from adl_drift.lang.java.detectors.routes import JavaRouteDetector
|
|
14
|
+
from adl_drift.lang.java.detectors.security import JavaSecurityDetector
|
|
15
|
+
from adl_drift.lang.java.detectors.tests import JavaTestDetector
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"JavaAnnotationDetector",
|
|
19
|
+
"JavaAsyncDetector",
|
|
20
|
+
"JavaConfigDetector",
|
|
21
|
+
"JavaDataAccessDetector",
|
|
22
|
+
"JavaDynamicDetector",
|
|
23
|
+
"JavaImportDetector",
|
|
24
|
+
"JavaLoggingDetector",
|
|
25
|
+
"JavaRouteDetector",
|
|
26
|
+
"JavaSecurityDetector",
|
|
27
|
+
"JavaTestDetector",
|
|
28
|
+
"JavaTypeDetector",
|
|
29
|
+
]
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"""Small Java detectors for the lower-volume shared categories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import re
|
|
7
|
+
from typing import Dict, List, Optional, Set
|
|
8
|
+
|
|
9
|
+
from tree_sitter import Node, Tree
|
|
10
|
+
|
|
11
|
+
from adl_drift.lang.java.detectors.base import DeclaredTypes, JavaDetector
|
|
12
|
+
from adl_drift.lang.java.scopes import TypeScopes
|
|
13
|
+
from adl_drift.utils.types import Pattern, PatternCategory
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class JavaAnnotationDetector(JavaDetector):
|
|
17
|
+
@property
|
|
18
|
+
def category(self) -> PatternCategory:
|
|
19
|
+
return PatternCategory.DECORATORS
|
|
20
|
+
|
|
21
|
+
def detect(self, tree: Tree, source_bytes: bytes, file_path: Path) -> List[Pattern]:
|
|
22
|
+
return [
|
|
23
|
+
self._make_pattern(
|
|
24
|
+
name="java_annotation",
|
|
25
|
+
description=f"Java annotation: @{self._annotation_name(node, source_bytes)}",
|
|
26
|
+
node=node,
|
|
27
|
+
file_path=file_path,
|
|
28
|
+
confidence=0.99,
|
|
29
|
+
metadata={
|
|
30
|
+
"annotation": self._annotation_name(node, source_bytes),
|
|
31
|
+
"reason": "Java annotation is explicit in source",
|
|
32
|
+
},
|
|
33
|
+
)
|
|
34
|
+
for node in self._walk(tree.root_node)
|
|
35
|
+
if node.type in ("annotation", "marker_annotation")
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class JavaConfigDetector(JavaDetector):
|
|
40
|
+
SPRING_PACKAGE = "org.springframework.beans.factory.annotation.Value"
|
|
41
|
+
PROPERTIES_PACKAGE = "org.springframework.boot.context.properties.ConfigurationProperties"
|
|
42
|
+
RESOURCE_FILE = re.compile(
|
|
43
|
+
r"^(?:application|bootstrap)(?:-[A-Za-z0-9_.-]+)?\.(?:properties|ya?ml)$",
|
|
44
|
+
re.IGNORECASE,
|
|
45
|
+
)
|
|
46
|
+
SPRING_RESOURCE_CONSTRUCTORS = {
|
|
47
|
+
"org.springframework.core.io.ClassPathResource",
|
|
48
|
+
"org.springframework.core.io.FileSystemResource",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def category(self) -> PatternCategory:
|
|
53
|
+
return PatternCategory.CONFIG
|
|
54
|
+
|
|
55
|
+
def detect(self, tree: Tree, source_bytes: bytes, file_path: Path) -> List[Pattern]:
|
|
56
|
+
root = tree.root_node
|
|
57
|
+
imports = self._imports(root, source_bytes)
|
|
58
|
+
types = self._declared_types(root, source_bytes)
|
|
59
|
+
local_types = self._declared_type_names(root, source_bytes)
|
|
60
|
+
patterns: List[Pattern] = []
|
|
61
|
+
for node in self._walk(root):
|
|
62
|
+
if node.type == "method_invocation":
|
|
63
|
+
method = self._text(node.child_by_field_name("name"), source_bytes)
|
|
64
|
+
receiver = self._text(node.child_by_field_name("object"), source_bytes)
|
|
65
|
+
if method in {"getenv", "getProperty"} and self._type_has_provenance(
|
|
66
|
+
receiver, imports, local_types, "java.lang.System", node
|
|
67
|
+
):
|
|
68
|
+
patterns.append(self._finding(
|
|
69
|
+
"environment_read", node, file_path, 0.99,
|
|
70
|
+
f"System.{method} reads external configuration", {"method": method},
|
|
71
|
+
))
|
|
72
|
+
resource = self._resource_invocation(
|
|
73
|
+
node, source_bytes, types, imports, local_types
|
|
74
|
+
)
|
|
75
|
+
if resource:
|
|
76
|
+
patterns.append(self._finding(
|
|
77
|
+
"config_resource_read", node, file_path, 0.96,
|
|
78
|
+
"Java resource API reads an application configuration file", resource,
|
|
79
|
+
))
|
|
80
|
+
elif node.type == "object_creation_expression":
|
|
81
|
+
resource = self._resource_constructor(node, source_bytes, imports, local_types)
|
|
82
|
+
if resource:
|
|
83
|
+
patterns.append(self._finding(
|
|
84
|
+
"config_resource_read", node, file_path, 0.97,
|
|
85
|
+
"Spring resource object targets an application configuration file", resource,
|
|
86
|
+
))
|
|
87
|
+
elif node.type in ("field_declaration", "class_declaration", "record_declaration"):
|
|
88
|
+
for annotation in self._annotations(node):
|
|
89
|
+
if self._annotation_is(annotation, source_bytes, imports, self.SPRING_PACKAGE):
|
|
90
|
+
raw_keys = self._annotation_strings(annotation, source_bytes)
|
|
91
|
+
patterns.append(self._finding(
|
|
92
|
+
"spring_value", node, file_path, 0.99,
|
|
93
|
+
"Field uses imported Spring @Value binding",
|
|
94
|
+
{
|
|
95
|
+
"keys": [self._normalize_key(key) for key in raw_keys],
|
|
96
|
+
"raw_keys": raw_keys,
|
|
97
|
+
},
|
|
98
|
+
))
|
|
99
|
+
elif self._annotation_is(annotation, source_bytes, imports, self.PROPERTIES_PACKAGE):
|
|
100
|
+
patterns.append(self._finding(
|
|
101
|
+
"configuration_properties", node, file_path, 0.99,
|
|
102
|
+
"Type uses imported Spring @ConfigurationProperties binding",
|
|
103
|
+
{"prefix": (self._annotation_strings(annotation, source_bytes) or [""])[0]},
|
|
104
|
+
))
|
|
105
|
+
return patterns
|
|
106
|
+
|
|
107
|
+
def _resource_invocation(
|
|
108
|
+
self, node: Node, source: bytes, types: DeclaredTypes,
|
|
109
|
+
imports: Set[str], local_types: TypeScopes,
|
|
110
|
+
) -> Optional[Dict[str, object]]:
|
|
111
|
+
resource_path = self._resource_path(node, source)
|
|
112
|
+
if not resource_path:
|
|
113
|
+
return None
|
|
114
|
+
method = self._text(node.child_by_field_name("name"), source)
|
|
115
|
+
receiver_node = node.child_by_field_name("object")
|
|
116
|
+
receiver = self._text(receiver_node, source)
|
|
117
|
+
declared = self._declared_type(receiver, types, node)
|
|
118
|
+
receiver_args = (
|
|
119
|
+
receiver_node.child_by_field_name("arguments")
|
|
120
|
+
if receiver_node is not None and receiver_node.type == "method_invocation"
|
|
121
|
+
else None
|
|
122
|
+
)
|
|
123
|
+
get_class_call = receiver_node is not None \
|
|
124
|
+
and receiver_node.type == "method_invocation" \
|
|
125
|
+
and self._text(receiver_node.child_by_field_name("name"), source) == "getClass" \
|
|
126
|
+
and receiver_args is not None and not receiver_args.named_children
|
|
127
|
+
class_resource = method in {"getResource", "getResourceAsStream"} and (
|
|
128
|
+
get_class_call or (receiver_node is not None and receiver_node.type == "class_literal")
|
|
129
|
+
)
|
|
130
|
+
classloader_resource = method in {
|
|
131
|
+
"getResource", "getResourceAsStream", "getSystemResource", "getSystemResourceAsStream",
|
|
132
|
+
} and (
|
|
133
|
+
self._type_has_provenance(
|
|
134
|
+
declared, imports, local_types, "java.lang.ClassLoader", node
|
|
135
|
+
) or self._type_has_provenance(
|
|
136
|
+
receiver, imports, local_types, "java.lang.ClassLoader", node
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
spring_resource = method == "getResource" and self._type_has_provenance(
|
|
140
|
+
declared, imports, local_types, "org.springframework.core.io.ResourceLoader", node
|
|
141
|
+
)
|
|
142
|
+
if not (class_resource or classloader_resource or spring_resource):
|
|
143
|
+
return None
|
|
144
|
+
return {"resource": resource_path, "api": method}
|
|
145
|
+
|
|
146
|
+
def _resource_constructor(
|
|
147
|
+
self, node: Node, source: bytes, imports: Set[str], local_types: TypeScopes,
|
|
148
|
+
) -> Optional[Dict[str, object]]:
|
|
149
|
+
resource_path = self._resource_path(node, source)
|
|
150
|
+
if not resource_path:
|
|
151
|
+
return None
|
|
152
|
+
raw_type = self._text(node.child_by_field_name("type"), source)
|
|
153
|
+
qualified = next(
|
|
154
|
+
(candidate for candidate in self.SPRING_RESOURCE_CONSTRUCTORS
|
|
155
|
+
if candidate.rsplit(".", 1)[-1] == self._simple_type(raw_type)),
|
|
156
|
+
"",
|
|
157
|
+
)
|
|
158
|
+
if not qualified or not self._type_has_provenance(
|
|
159
|
+
raw_type, imports, local_types, qualified, node
|
|
160
|
+
):
|
|
161
|
+
return None
|
|
162
|
+
return {"resource": resource_path, "api": self._simple_type(raw_type)}
|
|
163
|
+
|
|
164
|
+
def _resource_path(self, node: Node, source: bytes) -> str:
|
|
165
|
+
argument = self._first_argument(node)
|
|
166
|
+
if argument is None or argument.type != "string_literal":
|
|
167
|
+
return ""
|
|
168
|
+
raw = self._text(argument, source)
|
|
169
|
+
value = raw[1:-1] if len(raw) >= 2 else ""
|
|
170
|
+
filename = value.rsplit("/", 1)[-1]
|
|
171
|
+
return value if self.RESOURCE_FILE.fullmatch(filename) else ""
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def _normalize_key(value: str) -> str:
|
|
175
|
+
key = value
|
|
176
|
+
if key.startswith("${") and key.endswith("}"):
|
|
177
|
+
key = key[2:-1].split(":", 1)[0]
|
|
178
|
+
key = re.sub(r"([a-z0-9])([A-Z])", r"\1-\2", key)
|
|
179
|
+
return key.replace("_", "-").lower()
|
|
180
|
+
|
|
181
|
+
def _finding(
|
|
182
|
+
self, name: str, node: Node, path: Path, confidence: float,
|
|
183
|
+
reason: str, extra: Dict[str, object],
|
|
184
|
+
) -> Pattern:
|
|
185
|
+
return self._make_pattern(
|
|
186
|
+
name=name, description=reason, node=node, file_path=path,
|
|
187
|
+
confidence=confidence, metadata={"reason": reason, **extra},
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class JavaLoggingDetector(JavaDetector):
|
|
192
|
+
@property
|
|
193
|
+
def category(self) -> PatternCategory:
|
|
194
|
+
return PatternCategory.LOGGING
|
|
195
|
+
|
|
196
|
+
def detect(self, tree: Tree, source_bytes: bytes, file_path: Path) -> List[Pattern]:
|
|
197
|
+
imports = self._imports(tree.root_node, source_bytes)
|
|
198
|
+
types = self._declared_types(tree.root_node, source_bytes)
|
|
199
|
+
local_types = self._declared_type_names(tree.root_node, source_bytes)
|
|
200
|
+
patterns: List[Pattern] = []
|
|
201
|
+
for node in self._walk(tree.root_node):
|
|
202
|
+
if node.type != "method_invocation":
|
|
203
|
+
continue
|
|
204
|
+
method = self._text(node.child_by_field_name("name"), source_bytes)
|
|
205
|
+
receiver = self._text(node.child_by_field_name("object"), source_bytes)
|
|
206
|
+
system_type, separator, stream = receiver.rpartition(".")
|
|
207
|
+
if separator and stream in {"out", "err"} and method.startswith("print") \
|
|
208
|
+
and self._type_has_provenance(
|
|
209
|
+
system_type, imports, local_types, "java.lang.System", node
|
|
210
|
+
):
|
|
211
|
+
patterns.append(self._finding(
|
|
212
|
+
"system_output", node, file_path, 0.99,
|
|
213
|
+
"Direct System.out/System.err output", {"stream": receiver},
|
|
214
|
+
))
|
|
215
|
+
elif any(self._type_has_provenance(
|
|
216
|
+
self._declared_type(receiver, types, node), imports, local_types,
|
|
217
|
+
qualified, node,
|
|
218
|
+
) for qualified in (
|
|
219
|
+
"org.slf4j.Logger", "org.apache.logging.log4j.Logger",
|
|
220
|
+
"org.apache.log4j.Logger", "java.util.logging.Logger",
|
|
221
|
+
)) \
|
|
222
|
+
and method in {
|
|
223
|
+
"trace", "debug", "info", "warn", "error", "fatal", "log"
|
|
224
|
+
}:
|
|
225
|
+
first = self._first_argument(node)
|
|
226
|
+
eager = first is not None and first.type == "binary_expression"
|
|
227
|
+
patterns.append(self._finding(
|
|
228
|
+
"logger_call", node, file_path, 0.95,
|
|
229
|
+
"Call targets a declared logger",
|
|
230
|
+
{"level": method, "eager_concatenation": eager},
|
|
231
|
+
))
|
|
232
|
+
return patterns
|
|
233
|
+
|
|
234
|
+
def _finding(
|
|
235
|
+
self, name: str, node: Node, path: Path, confidence: float,
|
|
236
|
+
reason: str, extra: Dict[str, object],
|
|
237
|
+
) -> Pattern:
|
|
238
|
+
return self._make_pattern(
|
|
239
|
+
name=name, description=reason, node=node, file_path=path,
|
|
240
|
+
confidence=confidence, metadata={"reason": reason, **extra},
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class JavaTypeDetector(JavaDetector):
|
|
245
|
+
RAW_TYPES = {"Collection", "List", "Set", "Map", "Optional", "Class"}
|
|
246
|
+
NULLABILITY_ANNOTATIONS = {
|
|
247
|
+
"org.jetbrains.annotations.Nullable": "nullable",
|
|
248
|
+
"org.jetbrains.annotations.NotNull": "non_null",
|
|
249
|
+
"jakarta.annotation.Nullable": "nullable",
|
|
250
|
+
"javax.annotation.Nullable": "nullable",
|
|
251
|
+
"jakarta.validation.constraints.NotNull": "non_null",
|
|
252
|
+
"javax.validation.constraints.NotNull": "non_null",
|
|
253
|
+
"org.springframework.lang.Nullable": "nullable",
|
|
254
|
+
"org.springframework.lang.NonNull": "non_null",
|
|
255
|
+
"org.jspecify.annotations.Nullable": "nullable",
|
|
256
|
+
"org.jspecify.annotations.NonNull": "non_null",
|
|
257
|
+
"org.jspecify.annotations.NullMarked": "non_null_by_default",
|
|
258
|
+
"org.jspecify.annotations.NullUnmarked": "unspecified_by_default",
|
|
259
|
+
}
|
|
260
|
+
NULLABILITY_TARGETS = {
|
|
261
|
+
"field_declaration": "field",
|
|
262
|
+
"local_variable_declaration": "local",
|
|
263
|
+
"formal_parameter": "parameter",
|
|
264
|
+
"method_declaration": "return",
|
|
265
|
+
"class_declaration": "type",
|
|
266
|
+
"interface_declaration": "type",
|
|
267
|
+
"record_declaration": "type",
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def category(self) -> PatternCategory:
|
|
272
|
+
return PatternCategory.TYPES
|
|
273
|
+
|
|
274
|
+
def detect(self, tree: Tree, source_bytes: bytes, file_path: Path) -> List[Pattern]:
|
|
275
|
+
root = tree.root_node
|
|
276
|
+
imports = self._imports(root, source_bytes)
|
|
277
|
+
local_types = self._declared_type_names(root, source_bytes)
|
|
278
|
+
patterns: List[Pattern] = []
|
|
279
|
+
for node in self._walk(root):
|
|
280
|
+
if node.type in (
|
|
281
|
+
"field_declaration", "local_variable_declaration", "formal_parameter",
|
|
282
|
+
"method_declaration",
|
|
283
|
+
):
|
|
284
|
+
type_node = node.child_by_field_name("type")
|
|
285
|
+
type_name = self._text(type_node, source_bytes)
|
|
286
|
+
simple = self._simple_type(type_name)
|
|
287
|
+
qualified = f"java.lang.{simple}" if simple == "Class" else f"java.util.{simple}"
|
|
288
|
+
if "<" not in type_name and simple in self.RAW_TYPES and self._type_has_provenance(
|
|
289
|
+
type_name, imports, local_types, qualified, node
|
|
290
|
+
):
|
|
291
|
+
patterns.append(self._make_pattern(
|
|
292
|
+
name="raw_generic_type",
|
|
293
|
+
description=f"Raw generic type: {type_name}",
|
|
294
|
+
node=type_node or node,
|
|
295
|
+
file_path=file_path,
|
|
296
|
+
confidence=0.98,
|
|
297
|
+
metadata={
|
|
298
|
+
"type": type_name,
|
|
299
|
+
"reason": "Imported generic JDK type is used without type arguments",
|
|
300
|
+
},
|
|
301
|
+
))
|
|
302
|
+
target_kind = self.NULLABILITY_TARGETS.get(node.type)
|
|
303
|
+
if not target_kind:
|
|
304
|
+
continue
|
|
305
|
+
for annotation in self._annotations(node):
|
|
306
|
+
for qualified, nullability in self.NULLABILITY_ANNOTATIONS.items():
|
|
307
|
+
if not self._annotation_is(annotation, source_bytes, imports, qualified):
|
|
308
|
+
continue
|
|
309
|
+
patterns.append(self._make_pattern(
|
|
310
|
+
name="nullability_annotation",
|
|
311
|
+
description=f"Java nullability contract: {nullability}",
|
|
312
|
+
node=annotation,
|
|
313
|
+
file_path=file_path,
|
|
314
|
+
confidence=0.99,
|
|
315
|
+
metadata={
|
|
316
|
+
"nullability": nullability,
|
|
317
|
+
"target_kind": target_kind,
|
|
318
|
+
"annotation_fqn": qualified,
|
|
319
|
+
"reason": "Imported or fully-qualified nullability annotation",
|
|
320
|
+
},
|
|
321
|
+
))
|
|
322
|
+
break
|
|
323
|
+
return patterns
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class JavaAsyncDetector(JavaDetector):
|
|
327
|
+
SPRING_ASYNC = "org.springframework.scheduling.annotation.Async"
|
|
328
|
+
|
|
329
|
+
@property
|
|
330
|
+
def category(self) -> PatternCategory:
|
|
331
|
+
return PatternCategory.ASYNC
|
|
332
|
+
|
|
333
|
+
def detect(self, tree: Tree, source_bytes: bytes, file_path: Path) -> List[Pattern]:
|
|
334
|
+
root = tree.root_node
|
|
335
|
+
imports = self._imports(root, source_bytes)
|
|
336
|
+
types = self._declared_types(root, source_bytes)
|
|
337
|
+
local_types = self._declared_type_names(root, source_bytes)
|
|
338
|
+
patterns: List[Pattern] = []
|
|
339
|
+
for node in self._walk(root):
|
|
340
|
+
if node.type == "method_declaration" and any(
|
|
341
|
+
self._annotation_is(a, source_bytes, imports, self.SPRING_ASYNC)
|
|
342
|
+
for a in self._annotations(node)
|
|
343
|
+
):
|
|
344
|
+
patterns.append(self._finding(
|
|
345
|
+
"spring_async", node, file_path, 0.99, "Method uses imported Spring @Async",
|
|
346
|
+
))
|
|
347
|
+
elif node.type == "object_creation_expression" and self._type_has_provenance(
|
|
348
|
+
self._text(node.child_by_field_name("type"), source_bytes),
|
|
349
|
+
imports, local_types, "java.lang.Thread", node,
|
|
350
|
+
):
|
|
351
|
+
patterns.append(self._finding(
|
|
352
|
+
"raw_thread", node, file_path, 0.95, "Code constructs a raw java.lang.Thread",
|
|
353
|
+
))
|
|
354
|
+
elif node.type == "method_invocation":
|
|
355
|
+
method = self._text(node.child_by_field_name("name"), source_bytes)
|
|
356
|
+
receiver = self._text(node.child_by_field_name("object"), source_bytes)
|
|
357
|
+
raw_receiver_type = self._declared_type(receiver, types, node)
|
|
358
|
+
executor_type = self._simple_type(raw_receiver_type)
|
|
359
|
+
if method in {"submit", "execute", "invokeAll"} and executor_type in {
|
|
360
|
+
"Executor", "ExecutorService",
|
|
361
|
+
} and self._type_has_provenance(
|
|
362
|
+
raw_receiver_type, imports, local_types,
|
|
363
|
+
f"java.util.concurrent.{executor_type}", node,
|
|
364
|
+
):
|
|
365
|
+
patterns.append(self._finding(
|
|
366
|
+
"executor_task", node, file_path, 0.95,
|
|
367
|
+
"Declared Executor/ExecutorService schedules work",
|
|
368
|
+
))
|
|
369
|
+
elif method in {
|
|
370
|
+
"runAsync", "supplyAsync", "completedFuture", "allOf", "anyOf",
|
|
371
|
+
} and self._type_has_provenance(
|
|
372
|
+
receiver, imports, local_types,
|
|
373
|
+
"java.util.concurrent.CompletableFuture", node,
|
|
374
|
+
):
|
|
375
|
+
patterns.append(self._finding(
|
|
376
|
+
"completable_future", node, file_path, 0.98,
|
|
377
|
+
"CompletableFuture schedules or composes asynchronous work",
|
|
378
|
+
))
|
|
379
|
+
elif method in {
|
|
380
|
+
"thenApplyAsync", "thenAcceptAsync", "thenRunAsync", "handleAsync",
|
|
381
|
+
"whenCompleteAsync",
|
|
382
|
+
} and self._type_has_provenance(
|
|
383
|
+
self._declared_type(receiver, types, node), imports, local_types,
|
|
384
|
+
"java.util.concurrent.CompletableFuture", node,
|
|
385
|
+
):
|
|
386
|
+
patterns.append(self._finding(
|
|
387
|
+
"completable_future", node, file_path, 0.96,
|
|
388
|
+
"Declared CompletableFuture composes asynchronous work",
|
|
389
|
+
))
|
|
390
|
+
return patterns
|
|
391
|
+
|
|
392
|
+
def _finding(
|
|
393
|
+
self, name: str, node: Node, path: Path, confidence: float, reason: str
|
|
394
|
+
) -> Pattern:
|
|
395
|
+
return self._make_pattern(
|
|
396
|
+
name=name, description=reason, node=node, file_path=path,
|
|
397
|
+
confidence=confidence, metadata={"reason": reason},
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class JavaDynamicDetector(JavaDetector):
|
|
402
|
+
@property
|
|
403
|
+
def category(self) -> PatternCategory:
|
|
404
|
+
return PatternCategory.DYNAMIC
|
|
405
|
+
|
|
406
|
+
def detect(self, tree: Tree, source_bytes: bytes, file_path: Path) -> List[Pattern]:
|
|
407
|
+
imports = self._imports(tree.root_node, source_bytes)
|
|
408
|
+
types = self._declared_types(tree.root_node, source_bytes)
|
|
409
|
+
local_types = self._declared_type_names(tree.root_node, source_bytes)
|
|
410
|
+
patterns: List[Pattern] = []
|
|
411
|
+
for node in self._walk(tree.root_node):
|
|
412
|
+
if node.type != "method_invocation":
|
|
413
|
+
continue
|
|
414
|
+
method = self._text(node.child_by_field_name("name"), source_bytes)
|
|
415
|
+
receiver = self._text(node.child_by_field_name("object"), source_bytes)
|
|
416
|
+
name: Optional[str] = None
|
|
417
|
+
reason = ""
|
|
418
|
+
confidence = 0.9
|
|
419
|
+
if method == "forName" and self._type_has_provenance(
|
|
420
|
+
receiver, imports, local_types, "java.lang.Class", node
|
|
421
|
+
):
|
|
422
|
+
name, reason, confidence = "class_for_name", "Class.forName resolves a type at runtime", 0.98
|
|
423
|
+
elif method in {"getMethod", "getDeclaredMethod", "getField", "getDeclaredField"} \
|
|
424
|
+
and (receiver.endswith("getClass()")
|
|
425
|
+
or self._type_has_provenance(
|
|
426
|
+
self._declared_type(receiver, types, node), imports,
|
|
427
|
+
local_types, "java.lang.Class", node,
|
|
428
|
+
)):
|
|
429
|
+
name, reason = "reflective_lookup", "Member name is resolved through Java reflection"
|
|
430
|
+
elif method == "invoke" and self._has_import(imports, "java.lang.reflect.Method") \
|
|
431
|
+
and self._type_has_provenance(
|
|
432
|
+
self._declared_type(receiver, types, node), imports, local_types,
|
|
433
|
+
"java.lang.reflect.Method", node,
|
|
434
|
+
):
|
|
435
|
+
name, reason, confidence = "reflective_invoke", "Declared java.lang.reflect.Method is invoked", 0.96
|
|
436
|
+
elif method == "newProxyInstance" and self._type_has_provenance(
|
|
437
|
+
receiver, imports, local_types, "java.lang.reflect.Proxy", node):
|
|
438
|
+
name, reason, confidence = "dynamic_proxy", "java.lang.reflect.Proxy creates runtime dispatch", 0.98
|
|
439
|
+
if name:
|
|
440
|
+
patterns.append(self._make_pattern(
|
|
441
|
+
name=name, description=reason, node=node, file_path=file_path,
|
|
442
|
+
confidence=confidence,
|
|
443
|
+
metadata={"resolvable": "partial", "reason": reason},
|
|
444
|
+
))
|
|
445
|
+
return patterns
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Small CST helpers shared by Java detectors.
|
|
2
|
+
|
|
3
|
+
Import/declared-type provenance matching (`_imports`/`_declared_types`/
|
|
4
|
+
`_type_has_provenance`/`_annotation_is`) delegates to `lang/java/
|
|
5
|
+
type_provenance.py`, which P4.6's Java taint layer also uses directly
|
|
6
|
+
against a per-file `JavaTypeContext` — this class's methods are thin
|
|
7
|
+
wrappers kept for its many existing detector call sites, not a second
|
|
8
|
+
implementation.
|
|
9
|
+
|
|
10
|
+
`_imports`/`_declared_types`/`_declared_type_names` are memoized per file
|
|
11
|
+
(P8 Phase 2): up to 11 detector instances each call these at the top of
|
|
12
|
+
their own `detect()`, and `_annotation_is` (below) calls
|
|
13
|
+
`_declared_type_names` again on every single annotation check — profiling
|
|
14
|
+
showed this re-walking the same file's CST 20-30x instead of once. The
|
|
15
|
+
cache is a single-slot `threading.local()`, keyed by `root.id` — tree-sitter
|
|
16
|
+
Node objects are rewrapped on every attribute access (`tree.root_node is
|
|
17
|
+
tree.root_node` is `False`), so Python `is`/`id()` can't identify "same
|
|
18
|
+
underlying node" across the separate `tree.root_node` fetches each detector
|
|
19
|
+
makes; `root.id` is tree-sitter's own identifier for the underlying node and
|
|
20
|
+
*is* stable across those rewraps. `root.id` is itself address-derived and
|
|
21
|
+
in principle reusable once its tree is freed, but the slot also holds a
|
|
22
|
+
strong reference to the `root` it last cached (`_file_cache.root`) — as
|
|
23
|
+
long as that reference is live, its tree can't be freed, so its id can't be
|
|
24
|
+
handed to an unrelated new tree; the id can only repeat once the slot has
|
|
25
|
+
already moved on to caching something else. The cache naturally resets when
|
|
26
|
+
`DetectorRegistry.run_all()` moves to the next file, and stays correctly
|
|
27
|
+
isolated per worker under every `scanner/parallel.py` execution tier: a
|
|
28
|
+
fresh interpreter (and thread-local) per `ProcessPoolExecutor` worker, a
|
|
29
|
+
genuinely separate slot per `ThreadPoolExecutor` thread, and a single slot
|
|
30
|
+
reused file-by-file when sequential."""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import ast
|
|
35
|
+
import threading
|
|
36
|
+
from typing import Iterator, List, Optional, Set
|
|
37
|
+
|
|
38
|
+
from tree_sitter import Node
|
|
39
|
+
|
|
40
|
+
from adl_drift.detectors.base import BaseDetector
|
|
41
|
+
from adl_drift.lang.java import type_provenance as _tp
|
|
42
|
+
from adl_drift.lang.java.scopes import TYPE_DECLARATIONS, TypeScopes, declared_type_scopes
|
|
43
|
+
|
|
44
|
+
ANNOTATION_TYPES = ("annotation", "marker_annotation")
|
|
45
|
+
DECLARATION_TYPES = _tp._DECLARATION_TYPES
|
|
46
|
+
CLASS_TYPES = tuple(TYPE_DECLARATIONS)
|
|
47
|
+
SCOPE_TYPES = _tp._SCOPE_TYPES
|
|
48
|
+
DeclaredTypes = _tp.DeclaredTypes
|
|
49
|
+
|
|
50
|
+
_file_cache = threading.local()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _cache_for(root: Node) -> threading.local:
|
|
54
|
+
if getattr(_file_cache, "root_id", None) != root.id:
|
|
55
|
+
_file_cache.root_id = root.id
|
|
56
|
+
_file_cache.root = root
|
|
57
|
+
_file_cache.imports = None
|
|
58
|
+
_file_cache.declared_types = None
|
|
59
|
+
_file_cache.local_types = None
|
|
60
|
+
return _file_cache
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class JavaDetector(BaseDetector):
|
|
64
|
+
def _walk(self, node: Node) -> Iterator[Node]:
|
|
65
|
+
yield node
|
|
66
|
+
for child in node.children:
|
|
67
|
+
yield from self._walk(child)
|
|
68
|
+
|
|
69
|
+
def _text(self, node: Optional[Node], source: bytes) -> str:
|
|
70
|
+
return self.parser.node_text(node, source).strip() if node is not None else ""
|
|
71
|
+
|
|
72
|
+
def _annotations(self, node: Node) -> List[Node]:
|
|
73
|
+
modifiers = next((c for c in node.children if c.type == "modifiers"), None)
|
|
74
|
+
return [c for c in modifiers.children if c.type in ANNOTATION_TYPES] if modifiers else []
|
|
75
|
+
|
|
76
|
+
def _annotation_name(self, annotation: Node, source: bytes) -> str:
|
|
77
|
+
return self._text(annotation.child_by_field_name("name"), source)
|
|
78
|
+
|
|
79
|
+
def _annotation_strings(self, annotation: Node, source: bytes) -> List[str]:
|
|
80
|
+
values: List[str] = []
|
|
81
|
+
for node in self._walk(annotation):
|
|
82
|
+
if node.type != "string_literal":
|
|
83
|
+
continue
|
|
84
|
+
raw = self._text(node, source)
|
|
85
|
+
try:
|
|
86
|
+
values.append(ast.literal_eval(raw))
|
|
87
|
+
except (SyntaxError, ValueError):
|
|
88
|
+
values.append(raw.strip('"'))
|
|
89
|
+
return values
|
|
90
|
+
|
|
91
|
+
def _imports(self, root: Node, source: bytes) -> Set[str]:
|
|
92
|
+
cache = _cache_for(root)
|
|
93
|
+
if cache.imports is None:
|
|
94
|
+
cache.imports = _tp.imports_of(root, source)
|
|
95
|
+
return cache.imports
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def _has_import(imports: Set[str], qualified_name: str) -> bool:
|
|
99
|
+
return _tp.has_import(imports, qualified_name)
|
|
100
|
+
|
|
101
|
+
def _annotation_is(
|
|
102
|
+
self, annotation: Node, source: bytes, imports: Set[str], qualified_name: str
|
|
103
|
+
) -> bool:
|
|
104
|
+
name = self._annotation_name(annotation, source)
|
|
105
|
+
root = annotation
|
|
106
|
+
while root.parent is not None:
|
|
107
|
+
root = root.parent
|
|
108
|
+
local_types = self._declared_type_names(root, source)
|
|
109
|
+
return _tp.annotation_is(name, annotation.start_byte, imports, local_types, qualified_name)
|
|
110
|
+
|
|
111
|
+
def _declared_types(self, root: Node, source: bytes) -> DeclaredTypes:
|
|
112
|
+
cache = _cache_for(root)
|
|
113
|
+
if cache.declared_types is None:
|
|
114
|
+
cache.declared_types = _tp.declared_types_of(root, source)
|
|
115
|
+
return cache.declared_types
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def _typed_declaration_scope(node: Node) -> Optional[Node]:
|
|
119
|
+
return _tp._typed_declaration_scope(node)
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def _declaration_scope(node: Node) -> Node:
|
|
123
|
+
return _tp._declaration_scope(node)
|
|
124
|
+
|
|
125
|
+
def _declared_type(self, receiver: str, types: DeclaredTypes, at: Node) -> str:
|
|
126
|
+
return _tp.declared_type_of(receiver, types, at.start_byte)
|
|
127
|
+
|
|
128
|
+
def _declared_type_names(self, root: Node, source: bytes) -> TypeScopes:
|
|
129
|
+
cache = _cache_for(root)
|
|
130
|
+
if cache.local_types is None:
|
|
131
|
+
cache.local_types = declared_type_scopes(root, source)
|
|
132
|
+
return cache.local_types
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def _simple_type(type_name: str) -> str:
|
|
136
|
+
return _tp.simple_type(type_name)
|
|
137
|
+
|
|
138
|
+
def _type_has_provenance(
|
|
139
|
+
self, raw_type: str, imports: Set[str], local_types: TypeScopes,
|
|
140
|
+
qualified_name: str, at: Node,
|
|
141
|
+
) -> bool:
|
|
142
|
+
return _tp.type_has_provenance(raw_type, imports, local_types, qualified_name, at.start_byte)
|
|
143
|
+
|
|
144
|
+
def _first_argument(self, invocation: Node) -> Optional[Node]:
|
|
145
|
+
args = invocation.child_by_field_name("arguments")
|
|
146
|
+
if args is None:
|
|
147
|
+
return None
|
|
148
|
+
return next((c for c in args.named_children if not c.is_extra), None)
|
|
149
|
+
|
|
150
|
+
def _package(self, root: Node, source: bytes) -> str:
|
|
151
|
+
for node in root.named_children:
|
|
152
|
+
if node.type == "package_declaration":
|
|
153
|
+
return self._text(node.named_children[-1], source)
|
|
154
|
+
return "<default>"
|