apisec-code-bolt 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apisec_code_bolt/__init__.py +42 -0
- apisec_code_bolt/__main__.py +11 -0
- apisec_code_bolt/analysis/__init__.py +96 -0
- apisec_code_bolt/analysis/analyzer.py +2309 -0
- apisec_code_bolt/analysis/binding_tracker.py +341 -0
- apisec_code_bolt/analysis/call_graph.py +1197 -0
- apisec_code_bolt/analysis/call_graph_types.py +332 -0
- apisec_code_bolt/analysis/call_resolver.py +988 -0
- apisec_code_bolt/analysis/capability_tagger.py +322 -0
- apisec_code_bolt/analysis/config_scanner.py +197 -0
- apisec_code_bolt/analysis/data_flow.py +1883 -0
- apisec_code_bolt/analysis/dependency_extractor.py +959 -0
- apisec_code_bolt/analysis/flow_analysis.py +1406 -0
- apisec_code_bolt/analysis/hof_catalog.py +61 -0
- apisec_code_bolt/analysis/integration_detector.py +1399 -0
- apisec_code_bolt/analysis/literal_scanner.py +300 -0
- apisec_code_bolt/analysis/path_normalizer.py +55 -0
- apisec_code_bolt/analysis/read_site_detector.py +310 -0
- apisec_code_bolt/analysis/request_patterns.py +162 -0
- apisec_code_bolt/analysis/sensitivity_classifier.py +224 -0
- apisec_code_bolt/analysis/sink_evidence.py +333 -0
- apisec_code_bolt/analysis/url_prefix_resolver.py +338 -0
- apisec_code_bolt/cli/__init__.py +5 -0
- apisec_code_bolt/cli/exit_codes.py +17 -0
- apisec_code_bolt/cli/main.py +1069 -0
- apisec_code_bolt/cloud/__init__.py +1 -0
- apisec_code_bolt/cloud/apisec_client.py +118 -0
- apisec_code_bolt/cloud/client.py +255 -0
- apisec_code_bolt/core/__init__.py +75 -0
- apisec_code_bolt/core/config.py +528 -0
- apisec_code_bolt/core/credentials.py +65 -0
- apisec_code_bolt/core/discovery.py +433 -0
- apisec_code_bolt/core/log_format.py +115 -0
- apisec_code_bolt/core/manifest.py +1009 -0
- apisec_code_bolt/core/repo.py +280 -0
- apisec_code_bolt/core/state.py +59 -0
- apisec_code_bolt/core/telemetry.py +451 -0
- apisec_code_bolt/core/types.py +587 -0
- apisec_code_bolt/fingerprinting/__init__.py +1 -0
- apisec_code_bolt/frameworks/__init__.py +29 -0
- apisec_code_bolt/frameworks/_jwt_common.py +50 -0
- apisec_code_bolt/frameworks/auth_helpers.py +437 -0
- apisec_code_bolt/frameworks/base.py +608 -0
- apisec_code_bolt/frameworks/dotnet/__init__.py +17 -0
- apisec_code_bolt/frameworks/dotnet/_path_helpers.py +43 -0
- apisec_code_bolt/frameworks/dotnet/aspnet_plugin.py +2546 -0
- apisec_code_bolt/frameworks/dotnet/grpc_plugin.py +559 -0
- apisec_code_bolt/frameworks/dotnet/jwt_config_extractor.py +545 -0
- apisec_code_bolt/frameworks/dotnet/legacy_aspnet_plugin.py +732 -0
- apisec_code_bolt/frameworks/dotnet/refit_plugin.py +374 -0
- apisec_code_bolt/frameworks/dotnet/wcf_plugin.py +1239 -0
- apisec_code_bolt/frameworks/java/__init__.py +6 -0
- apisec_code_bolt/frameworks/java/_annotations.py +167 -0
- apisec_code_bolt/frameworks/java/_constraints.py +128 -0
- apisec_code_bolt/frameworks/java/graphql_plugin.py +287 -0
- apisec_code_bolt/frameworks/java/jaxrs_plugin.py +748 -0
- apisec_code_bolt/frameworks/java/jwt_config_extractor.py +361 -0
- apisec_code_bolt/frameworks/java/micronaut_plugin.py +1059 -0
- apisec_code_bolt/frameworks/java/spring_plugin.py +1293 -0
- apisec_code_bolt/frameworks/js/__init__.py +8 -0
- apisec_code_bolt/frameworks/js/express_plugin.py +391 -0
- apisec_code_bolt/frameworks/js/fastify_plugin.py +381 -0
- apisec_code_bolt/frameworks/js/graphql_plugin.py +198 -0
- apisec_code_bolt/frameworks/js/nestjs_plugin.py +423 -0
- apisec_code_bolt/frameworks/python/__init__.py +19 -0
- apisec_code_bolt/frameworks/python/celery_plugin.py +393 -0
- apisec_code_bolt/frameworks/python/click_plugin.py +427 -0
- apisec_code_bolt/frameworks/python/django_plugin.py +867 -0
- apisec_code_bolt/frameworks/python/fastapi/__init__.py +28 -0
- apisec_code_bolt/frameworks/python/fastapi/plugin.py +1390 -0
- apisec_code_bolt/frameworks/python/flask_plugin.py +205 -0
- apisec_code_bolt/frameworks/python/graphql_plugin.py +274 -0
- apisec_code_bolt/frameworks/python/prefect_plugin.py +251 -0
- apisec_code_bolt/frameworks/python/webhook_plugin.py +255 -0
- apisec_code_bolt/parsing/__init__.py +62 -0
- apisec_code_bolt/parsing/base.py +554 -0
- apisec_code_bolt/parsing/csharp/__init__.py +5 -0
- apisec_code_bolt/parsing/csharp/language_services.py +203 -0
- apisec_code_bolt/parsing/csharp/literals.py +72 -0
- apisec_code_bolt/parsing/csharp/parser.py +1158 -0
- apisec_code_bolt/parsing/csharp/type_resolver.py +568 -0
- apisec_code_bolt/parsing/js/__init__.py +5 -0
- apisec_code_bolt/parsing/js/language_services.py +118 -0
- apisec_code_bolt/parsing/js/parser.py +622 -0
- apisec_code_bolt/parsing/jvm/__init__.py +7 -0
- apisec_code_bolt/parsing/jvm/language_services.py +270 -0
- apisec_code_bolt/parsing/jvm/parser.py +774 -0
- apisec_code_bolt/parsing/jvm/type_resolver.py +422 -0
- apisec_code_bolt/parsing/python/__init__.py +150 -0
- apisec_code_bolt/parsing/python/cbv_extractor.py +606 -0
- apisec_code_bolt/parsing/python/constant_resolver.py +500 -0
- apisec_code_bolt/parsing/python/cross_file_resolver.py +1054 -0
- apisec_code_bolt/parsing/python/dynamic_route_detector.py +532 -0
- apisec_code_bolt/parsing/python/expression_utils.py +221 -0
- apisec_code_bolt/parsing/python/extraction_types.py +271 -0
- apisec_code_bolt/parsing/python/language_services.py +487 -0
- apisec_code_bolt/parsing/python/parameter_analyzer.py +789 -0
- apisec_code_bolt/parsing/python/parser.py +719 -0
- apisec_code_bolt/parsing/python/path_resolver.py +576 -0
- apisec_code_bolt/parsing/python/router_registry.py +806 -0
- apisec_code_bolt/parsing/python/type_resolver.py +730 -0
- apisec_code_bolt/parsing/python/visitors.py +1544 -0
- apisec_code_bolt/parsing/services.py +544 -0
- apisec_code_bolt/query/__init__.py +1 -0
- apisec_code_bolt/query/ast_cache.py +182 -0
- apisec_code_bolt/query/executor.py +283 -0
- apisec_code_bolt/query/handlers.py +832 -0
- apisec_code_bolt-0.1.0.dist-info/METADATA +230 -0
- apisec_code_bolt-0.1.0.dist-info/RECORD +111 -0
- apisec_code_bolt-0.1.0.dist-info/WHEEL +4 -0
- apisec_code_bolt-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JAX-RS framework plugin.
|
|
3
|
+
|
|
4
|
+
Extracts HTTP routes, auth schemes, dependencies, and middleware from
|
|
5
|
+
JAX-RS applications (Jersey, Quarkus REST, RESTEasy, etc.).
|
|
6
|
+
|
|
7
|
+
Supports:
|
|
8
|
+
- javax.ws.rs.* (Java EE 8) and jakarta.ws.rs.* (Jakarta EE 9+)
|
|
9
|
+
- @Path on class + method (joined as prefix/method-segment)
|
|
10
|
+
- @GET, @POST, @PUT, @DELETE, @PATCH, @HEAD, @OPTIONS HTTP verb annotations
|
|
11
|
+
- @ApplicationPath as a global URI prefix (Jersey / Quarkus Application subclass)
|
|
12
|
+
- Sub-resource locators: @Path without HTTP verb are skipped (not routes)
|
|
13
|
+
- @PathParam, @QueryParam, @HeaderParam, @CookieParam, @FormParam, @BeanParam
|
|
14
|
+
- Implicit body: unannotated, non-scalar parameters treated as request body
|
|
15
|
+
- @Produces / @Consumes for content-type metadata
|
|
16
|
+
- @RolesAllowed, @PermitAll, @DenyAll (javax.annotation.security) access control
|
|
17
|
+
- ContainerRequestFilter / ContainerResponseFilter middleware detection
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
import re
|
|
24
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
25
|
+
|
|
26
|
+
from ...core.types import (
|
|
27
|
+
AuthDependencyType,
|
|
28
|
+
AuthSchemeType,
|
|
29
|
+
CodeLocation,
|
|
30
|
+
Confidence,
|
|
31
|
+
Framework,
|
|
32
|
+
HttpMethod,
|
|
33
|
+
Language,
|
|
34
|
+
ParameterLocation,
|
|
35
|
+
)
|
|
36
|
+
from ...parsing.base import ParsedClass, ParsedDecorator, ParsedFile, ParsedFunction
|
|
37
|
+
from ...parsing.services import AnalysisContext
|
|
38
|
+
from ..base import (
|
|
39
|
+
BaseFrameworkPlugin,
|
|
40
|
+
ExtractedAuthDependency,
|
|
41
|
+
ExtractedAuthScheme,
|
|
42
|
+
ExtractedBody,
|
|
43
|
+
ExtractedDependency,
|
|
44
|
+
ExtractedJwtConfig,
|
|
45
|
+
ExtractedMiddleware,
|
|
46
|
+
ExtractedParameter,
|
|
47
|
+
ExtractedResponse,
|
|
48
|
+
ExtractedRoute,
|
|
49
|
+
FrameworkPluginRegistry,
|
|
50
|
+
)
|
|
51
|
+
from .jwt_config_extractor import JavaJwtConfigExtractor
|
|
52
|
+
|
|
53
|
+
if TYPE_CHECKING:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
logger = logging.getLogger(__name__)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# =============================================================================
|
|
60
|
+
# Constants
|
|
61
|
+
# =============================================================================
|
|
62
|
+
|
|
63
|
+
# JAX-RS HTTP verb annotation → HttpMethod
|
|
64
|
+
_VERB_TO_METHOD: dict[str, HttpMethod] = {
|
|
65
|
+
"GET": HttpMethod.GET,
|
|
66
|
+
"POST": HttpMethod.POST,
|
|
67
|
+
"PUT": HttpMethod.PUT,
|
|
68
|
+
"DELETE": HttpMethod.DELETE,
|
|
69
|
+
"PATCH": HttpMethod.PATCH,
|
|
70
|
+
"HEAD": HttpMethod.HEAD,
|
|
71
|
+
"OPTIONS": HttpMethod.OPTIONS,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Security annotations from javax.annotation.security / jakarta.annotation.security
|
|
75
|
+
_SECURITY_ANNOTATIONS: frozenset[str] = frozenset(
|
|
76
|
+
{
|
|
77
|
+
"RolesAllowed",
|
|
78
|
+
"PermitAll",
|
|
79
|
+
"DenyAll",
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# JAX-RS filter base types (middleware)
|
|
84
|
+
_FILTER_BASE_TYPES: frozenset[str] = frozenset(
|
|
85
|
+
{
|
|
86
|
+
"ContainerRequestFilter",
|
|
87
|
+
"ContainerResponseFilter",
|
|
88
|
+
"ClientRequestFilter",
|
|
89
|
+
"ClientResponseFilter",
|
|
90
|
+
"WriterInterceptor",
|
|
91
|
+
"ReaderInterceptor",
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
_AUTH_RELATED_NAMES: frozenset[str] = frozenset(
|
|
96
|
+
{
|
|
97
|
+
"auth",
|
|
98
|
+
"authentication",
|
|
99
|
+
"authorization",
|
|
100
|
+
"security",
|
|
101
|
+
"jwt",
|
|
102
|
+
"token",
|
|
103
|
+
"user",
|
|
104
|
+
"principal",
|
|
105
|
+
"credential",
|
|
106
|
+
"login",
|
|
107
|
+
"filter",
|
|
108
|
+
"interceptor",
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Simple JVM scalar types — unannotated params of these types are NOT implicit bodies
|
|
113
|
+
_SCALAR_TYPES: frozenset[str] = frozenset(
|
|
114
|
+
{
|
|
115
|
+
"String",
|
|
116
|
+
"string",
|
|
117
|
+
"int",
|
|
118
|
+
"Integer",
|
|
119
|
+
"long",
|
|
120
|
+
"Long",
|
|
121
|
+
"boolean",
|
|
122
|
+
"Boolean",
|
|
123
|
+
"double",
|
|
124
|
+
"Double",
|
|
125
|
+
"float",
|
|
126
|
+
"Float",
|
|
127
|
+
"byte",
|
|
128
|
+
"Byte",
|
|
129
|
+
"char",
|
|
130
|
+
"Character",
|
|
131
|
+
"short",
|
|
132
|
+
"Short",
|
|
133
|
+
"BigDecimal",
|
|
134
|
+
"BigInteger",
|
|
135
|
+
"UUID",
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Framework-injected JAX-RS context types — skip these when inferring params
|
|
140
|
+
_INJECTED_TYPES: frozenset[str] = frozenset(
|
|
141
|
+
{
|
|
142
|
+
"UriInfo",
|
|
143
|
+
"HttpHeaders",
|
|
144
|
+
"Request",
|
|
145
|
+
"SecurityContext",
|
|
146
|
+
"Providers",
|
|
147
|
+
"Application",
|
|
148
|
+
"ResourceContext",
|
|
149
|
+
"Configuration",
|
|
150
|
+
"AsyncResponse",
|
|
151
|
+
"HttpServletRequest",
|
|
152
|
+
"HttpServletResponse",
|
|
153
|
+
"ServletContext",
|
|
154
|
+
"Principal",
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# DETECTION_IMPORTS — files with these import prefixes are JAX-RS files
|
|
159
|
+
_JAXRS_IMPORTS: frozenset[str] = frozenset(
|
|
160
|
+
{
|
|
161
|
+
"javax.ws.rs",
|
|
162
|
+
"jakarta.ws.rs",
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# =============================================================================
|
|
168
|
+
# JaxRsPlugin
|
|
169
|
+
# =============================================================================
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class JaxRsPlugin(BaseFrameworkPlugin):
|
|
173
|
+
"""
|
|
174
|
+
Framework plugin for JAX-RS applications.
|
|
175
|
+
|
|
176
|
+
Covers Jersey, Quarkus REST, RESTEasy, and any spec-compliant
|
|
177
|
+
implementation using javax.ws.rs or jakarta.ws.rs annotations.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
FRAMEWORK: ClassVar[Framework] = Framework.JAXRS
|
|
181
|
+
LANGUAGE: ClassVar[Language] = Language.JAVA
|
|
182
|
+
DETECTION_IMPORTS: ClassVar[frozenset[str]] = _JAXRS_IMPORTS
|
|
183
|
+
|
|
184
|
+
# -------------------------------------------------------------------------
|
|
185
|
+
# Detection
|
|
186
|
+
# -------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
def detect(self, parsed_file: ParsedFile) -> bool:
|
|
189
|
+
for imp in parsed_file.imports:
|
|
190
|
+
module = imp.module or ""
|
|
191
|
+
if module.startswith("javax.ws.rs") or module.startswith("jakarta.ws.rs"):
|
|
192
|
+
return True
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
# -------------------------------------------------------------------------
|
|
196
|
+
# Route extraction
|
|
197
|
+
# -------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
def extract_routes(
|
|
200
|
+
self,
|
|
201
|
+
parsed_file: ParsedFile,
|
|
202
|
+
context: AnalysisContext | None = None,
|
|
203
|
+
) -> list[ExtractedRoute]:
|
|
204
|
+
routes: list[ExtractedRoute] = []
|
|
205
|
+
|
|
206
|
+
# Collect the @ApplicationPath global prefix if present in this file.
|
|
207
|
+
# In Jersey / Quarkus the Application subclass and resource classes are
|
|
208
|
+
# usually in separate files, so this will often be empty — that is fine.
|
|
209
|
+
app_prefix = self._application_path_prefix(parsed_file)
|
|
210
|
+
|
|
211
|
+
for cls in parsed_file.classes:
|
|
212
|
+
class_path_dec = self._find_annotation(cls.decorators, "Path")
|
|
213
|
+
if class_path_dec is None:
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
class_segment = self._annotation_path(class_path_dec) or ""
|
|
217
|
+
class_prefix = self._join_paths(app_prefix, class_segment)
|
|
218
|
+
|
|
219
|
+
class_security_anns = [d for d in cls.decorators if d.name in _SECURITY_ANNOTATIONS]
|
|
220
|
+
|
|
221
|
+
for method in cls.methods:
|
|
222
|
+
method_routes = self._extract_method_routes(
|
|
223
|
+
method,
|
|
224
|
+
class_prefix,
|
|
225
|
+
cls,
|
|
226
|
+
parsed_file,
|
|
227
|
+
context,
|
|
228
|
+
class_security_anns=class_security_anns,
|
|
229
|
+
)
|
|
230
|
+
routes.extend(method_routes)
|
|
231
|
+
|
|
232
|
+
return routes
|
|
233
|
+
|
|
234
|
+
def _application_path_prefix(self, parsed_file: ParsedFile) -> str:
|
|
235
|
+
"""Return the @ApplicationPath value if this file contains one, else ''."""
|
|
236
|
+
for cls in parsed_file.classes:
|
|
237
|
+
for dec in cls.decorators:
|
|
238
|
+
if dec.name == "ApplicationPath":
|
|
239
|
+
path = self._annotation_path(dec)
|
|
240
|
+
if path:
|
|
241
|
+
return path.rstrip("/")
|
|
242
|
+
return ""
|
|
243
|
+
|
|
244
|
+
def _extract_method_routes(
|
|
245
|
+
self,
|
|
246
|
+
method: ParsedFunction,
|
|
247
|
+
class_prefix: str,
|
|
248
|
+
cls: ParsedClass,
|
|
249
|
+
parsed_file: ParsedFile,
|
|
250
|
+
context: AnalysisContext | None,
|
|
251
|
+
class_security_anns: list[ParsedDecorator],
|
|
252
|
+
) -> list[ExtractedRoute]:
|
|
253
|
+
# Collect the HTTP verb annotation(s) for this method
|
|
254
|
+
http_verbs: list[tuple[str, HttpMethod]] = [
|
|
255
|
+
(dec.name, _VERB_TO_METHOD[dec.name])
|
|
256
|
+
for dec in method.decorators
|
|
257
|
+
if dec.name in _VERB_TO_METHOD
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
if not http_verbs:
|
|
261
|
+
# @Path without a verb = sub-resource locator — skip
|
|
262
|
+
return []
|
|
263
|
+
|
|
264
|
+
# Method-level @Path is optional; missing = same as class prefix
|
|
265
|
+
method_path_dec = self._find_annotation(method.decorators, "Path")
|
|
266
|
+
method_segment = self._annotation_path(method_path_dec) if method_path_dec else ""
|
|
267
|
+
full_path = self._join_paths(class_prefix, method_segment or "")
|
|
268
|
+
|
|
269
|
+
produces = self._annotation_str_from_decorators(method.decorators, "Produces")
|
|
270
|
+
consumes = self._annotation_str_from_decorators(method.decorators, "Consumes")
|
|
271
|
+
if produces is None:
|
|
272
|
+
produces = self._annotation_str_from_class(cls, "Produces")
|
|
273
|
+
if consumes is None:
|
|
274
|
+
consumes = self._annotation_str_from_class(cls, "Consumes")
|
|
275
|
+
|
|
276
|
+
# Auth refs
|
|
277
|
+
auth_refs = self._collect_auth_refs(cls, method, class_security_anns)
|
|
278
|
+
|
|
279
|
+
path_params, query_params, header_params, cookie_params, body = self._extract_method_params(
|
|
280
|
+
method, full_path, consumes, context, parsed_file
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
routes: list[ExtractedRoute] = []
|
|
284
|
+
for _verb_name, http_method in http_verbs:
|
|
285
|
+
routes.append(
|
|
286
|
+
ExtractedRoute(
|
|
287
|
+
method=http_method,
|
|
288
|
+
path=full_path,
|
|
289
|
+
handler_function=method.qualified_name,
|
|
290
|
+
handler_location=method.location,
|
|
291
|
+
path_params=list(path_params),
|
|
292
|
+
query_params=list(query_params),
|
|
293
|
+
header_params=list(header_params),
|
|
294
|
+
cookie_params=list(cookie_params),
|
|
295
|
+
body=body,
|
|
296
|
+
response=ExtractedResponse(
|
|
297
|
+
model_name=method.return_type,
|
|
298
|
+
status_code=200,
|
|
299
|
+
content_type=produces,
|
|
300
|
+
),
|
|
301
|
+
tags=[cls.name],
|
|
302
|
+
dependency_refs=list(auth_refs),
|
|
303
|
+
confidence=Confidence.HIGH,
|
|
304
|
+
kind="http",
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
return routes
|
|
308
|
+
|
|
309
|
+
def _collect_auth_refs(
|
|
310
|
+
self,
|
|
311
|
+
cls: ParsedClass,
|
|
312
|
+
method: ParsedFunction,
|
|
313
|
+
class_security_anns: list[ParsedDecorator],
|
|
314
|
+
) -> list[str]:
|
|
315
|
+
refs: list[str] = []
|
|
316
|
+
if class_security_anns:
|
|
317
|
+
refs.append(cls.name)
|
|
318
|
+
method_security = [d for d in method.decorators if d.name in _SECURITY_ANNOTATIONS]
|
|
319
|
+
if method_security:
|
|
320
|
+
refs.append(f"{cls.name}.{method.name}")
|
|
321
|
+
return refs
|
|
322
|
+
|
|
323
|
+
# -------------------------------------------------------------------------
|
|
324
|
+
# Parameter extraction
|
|
325
|
+
# -------------------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
def _extract_method_params(
|
|
328
|
+
self,
|
|
329
|
+
method: ParsedFunction,
|
|
330
|
+
path: str,
|
|
331
|
+
consumes: str | None,
|
|
332
|
+
context: AnalysisContext | None,
|
|
333
|
+
parsed_file: ParsedFile,
|
|
334
|
+
) -> tuple[
|
|
335
|
+
list[ExtractedParameter],
|
|
336
|
+
list[ExtractedParameter],
|
|
337
|
+
list[ExtractedParameter],
|
|
338
|
+
list[ExtractedParameter],
|
|
339
|
+
ExtractedBody | None,
|
|
340
|
+
]:
|
|
341
|
+
path_params: list[ExtractedParameter] = []
|
|
342
|
+
query_params: list[ExtractedParameter] = []
|
|
343
|
+
header_params: list[ExtractedParameter] = []
|
|
344
|
+
cookie_params: list[ExtractedParameter] = []
|
|
345
|
+
body: ExtractedBody | None = None
|
|
346
|
+
|
|
347
|
+
path_template_names = set(re.findall(r"\{([^}:]+)(?::[^}]+)?\}", path))
|
|
348
|
+
form_parts: list[str] = []
|
|
349
|
+
|
|
350
|
+
for param in method.parameters:
|
|
351
|
+
metadata = param.metadata or {}
|
|
352
|
+
|
|
353
|
+
if "PathParam" in metadata:
|
|
354
|
+
alias = metadata["PathParam"]
|
|
355
|
+
name = alias if isinstance(alias, str) and alias else param.name
|
|
356
|
+
path_params.append(
|
|
357
|
+
ExtractedParameter(
|
|
358
|
+
name=name,
|
|
359
|
+
location=ParameterLocation.PATH,
|
|
360
|
+
type_annotation=param.type_annotation,
|
|
361
|
+
required=True,
|
|
362
|
+
code_location=param.location,
|
|
363
|
+
)
|
|
364
|
+
)
|
|
365
|
+
continue
|
|
366
|
+
|
|
367
|
+
if "QueryParam" in metadata:
|
|
368
|
+
alias = metadata["QueryParam"]
|
|
369
|
+
name = alias if isinstance(alias, str) and alias else param.name
|
|
370
|
+
query_params.append(
|
|
371
|
+
ExtractedParameter(
|
|
372
|
+
name=name,
|
|
373
|
+
location=ParameterLocation.QUERY,
|
|
374
|
+
type_annotation=param.type_annotation,
|
|
375
|
+
required=False,
|
|
376
|
+
code_location=param.location,
|
|
377
|
+
)
|
|
378
|
+
)
|
|
379
|
+
continue
|
|
380
|
+
|
|
381
|
+
if "HeaderParam" in metadata:
|
|
382
|
+
alias = metadata["HeaderParam"]
|
|
383
|
+
name = alias if isinstance(alias, str) and alias else param.name
|
|
384
|
+
header_params.append(
|
|
385
|
+
ExtractedParameter(
|
|
386
|
+
name=name,
|
|
387
|
+
location=ParameterLocation.HEADER,
|
|
388
|
+
type_annotation=param.type_annotation,
|
|
389
|
+
required=True,
|
|
390
|
+
code_location=param.location,
|
|
391
|
+
)
|
|
392
|
+
)
|
|
393
|
+
continue
|
|
394
|
+
|
|
395
|
+
if "CookieParam" in metadata:
|
|
396
|
+
alias = metadata["CookieParam"]
|
|
397
|
+
name = alias if isinstance(alias, str) and alias else param.name
|
|
398
|
+
cookie_params.append(
|
|
399
|
+
ExtractedParameter(
|
|
400
|
+
name=name,
|
|
401
|
+
location=ParameterLocation.COOKIE,
|
|
402
|
+
type_annotation=param.type_annotation,
|
|
403
|
+
required=True,
|
|
404
|
+
code_location=param.location,
|
|
405
|
+
)
|
|
406
|
+
)
|
|
407
|
+
continue
|
|
408
|
+
|
|
409
|
+
if "FormParam" in metadata:
|
|
410
|
+
alias = metadata["FormParam"]
|
|
411
|
+
name = alias if isinstance(alias, str) and alias else param.name
|
|
412
|
+
form_parts.append(name)
|
|
413
|
+
continue
|
|
414
|
+
|
|
415
|
+
if "BeanParam" in metadata:
|
|
416
|
+
# Aggregate bean — treat as body with the bean type
|
|
417
|
+
if body is None:
|
|
418
|
+
body = ExtractedBody(
|
|
419
|
+
content_type=consumes or "application/x-www-form-urlencoded",
|
|
420
|
+
model_name=param.type_annotation,
|
|
421
|
+
required=True,
|
|
422
|
+
)
|
|
423
|
+
continue
|
|
424
|
+
|
|
425
|
+
# No JAX-RS annotation — decide by convention
|
|
426
|
+
if param.type_annotation in _INJECTED_TYPES:
|
|
427
|
+
continue
|
|
428
|
+
|
|
429
|
+
if param.name in path_template_names:
|
|
430
|
+
path_params.append(
|
|
431
|
+
ExtractedParameter(
|
|
432
|
+
name=param.name,
|
|
433
|
+
location=ParameterLocation.PATH,
|
|
434
|
+
type_annotation=param.type_annotation,
|
|
435
|
+
required=True,
|
|
436
|
+
code_location=param.location,
|
|
437
|
+
)
|
|
438
|
+
)
|
|
439
|
+
continue
|
|
440
|
+
|
|
441
|
+
# Unannotated non-scalar = implicit entity body
|
|
442
|
+
if param.type_annotation and param.type_annotation not in _SCALAR_TYPES:
|
|
443
|
+
if body is None:
|
|
444
|
+
model_fields: list[str] = []
|
|
445
|
+
if context and context.type_resolver and param.type_annotation:
|
|
446
|
+
resolved = context.type_resolver.get_model_fields(
|
|
447
|
+
param.type_annotation, parsed_file.path
|
|
448
|
+
)
|
|
449
|
+
model_fields = [f.name for f in resolved]
|
|
450
|
+
body = ExtractedBody(
|
|
451
|
+
content_type=consumes or "application/json",
|
|
452
|
+
model_name=param.type_annotation,
|
|
453
|
+
model_fields=model_fields,
|
|
454
|
+
required=True,
|
|
455
|
+
)
|
|
456
|
+
continue
|
|
457
|
+
|
|
458
|
+
# Unannotated scalar — treat as implicit query param
|
|
459
|
+
if param.type_annotation and param.type_annotation in _SCALAR_TYPES:
|
|
460
|
+
query_params.append(
|
|
461
|
+
ExtractedParameter(
|
|
462
|
+
name=param.name,
|
|
463
|
+
location=ParameterLocation.QUERY,
|
|
464
|
+
type_annotation=param.type_annotation,
|
|
465
|
+
required=False,
|
|
466
|
+
code_location=param.location,
|
|
467
|
+
)
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
# Collect @FormParam fields into a form body (if no explicit body yet)
|
|
471
|
+
if form_parts and body is None:
|
|
472
|
+
body = ExtractedBody(
|
|
473
|
+
content_type="application/x-www-form-urlencoded",
|
|
474
|
+
model_fields=form_parts,
|
|
475
|
+
required=True,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
return path_params, query_params, header_params, cookie_params, body
|
|
479
|
+
|
|
480
|
+
# -------------------------------------------------------------------------
|
|
481
|
+
# Dependency extraction
|
|
482
|
+
# -------------------------------------------------------------------------
|
|
483
|
+
|
|
484
|
+
def extract_dependencies(self, parsed_file: ParsedFile) -> list[ExtractedDependency]:
|
|
485
|
+
deps: list[ExtractedDependency] = []
|
|
486
|
+
for cls in parsed_file.classes:
|
|
487
|
+
ann_names = {d.name for d in cls.decorators}
|
|
488
|
+
# JAX-RS doesn't have a standard DI annotation — detect by
|
|
489
|
+
# @Path (resource class) or CDI @ApplicationScoped / @RequestScoped
|
|
490
|
+
is_resource = "Path" in ann_names
|
|
491
|
+
is_cdi_bean = bool(
|
|
492
|
+
ann_names & {"ApplicationScoped", "RequestScoped", "Singleton", "Dependent"}
|
|
493
|
+
)
|
|
494
|
+
if is_resource or is_cdi_bean:
|
|
495
|
+
is_auth = any(kw in cls.name.lower() for kw in _AUTH_RELATED_NAMES)
|
|
496
|
+
deps.append(
|
|
497
|
+
ExtractedDependency(
|
|
498
|
+
name=cls.name,
|
|
499
|
+
qualified_name=cls.qualified_name,
|
|
500
|
+
location=cls.location,
|
|
501
|
+
dependency_type="class",
|
|
502
|
+
provides_type=cls.name,
|
|
503
|
+
is_auth_related=is_auth,
|
|
504
|
+
confidence=Confidence.HIGH,
|
|
505
|
+
)
|
|
506
|
+
)
|
|
507
|
+
return deps
|
|
508
|
+
|
|
509
|
+
# -------------------------------------------------------------------------
|
|
510
|
+
# Auth scheme extraction
|
|
511
|
+
# -------------------------------------------------------------------------
|
|
512
|
+
|
|
513
|
+
def extract_auth_schemes(self, parsed_file: ParsedFile) -> list[ExtractedAuthScheme]:
|
|
514
|
+
schemes: list[ExtractedAuthScheme] = []
|
|
515
|
+
|
|
516
|
+
for cls in parsed_file.classes:
|
|
517
|
+
name_lower = cls.name.lower()
|
|
518
|
+
# Heuristic: class name suggests auth config
|
|
519
|
+
if any(kw in name_lower for kw in ("security", "auth", "jwt", "oauth")):
|
|
520
|
+
scheme_type = AuthSchemeType.JWT_BEARER
|
|
521
|
+
if "oauth" in name_lower:
|
|
522
|
+
scheme_type = AuthSchemeType.OAUTH2_AUTHORIZATION_CODE
|
|
523
|
+
schemes.append(
|
|
524
|
+
ExtractedAuthScheme(
|
|
525
|
+
scheme_type=scheme_type,
|
|
526
|
+
name=cls.name,
|
|
527
|
+
location=cls.location,
|
|
528
|
+
config={"class": cls.name},
|
|
529
|
+
confidence=Confidence.MEDIUM,
|
|
530
|
+
)
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
# Import-based: MicroProfile JWT, SmallRye JWT, JJWT
|
|
534
|
+
for imp in parsed_file.imports:
|
|
535
|
+
module = imp.module or ""
|
|
536
|
+
if any(
|
|
537
|
+
tok in module
|
|
538
|
+
for tok in ("microprofile.jwt", "smallrye.jwt", "jjwt", "nimbus", "auth0.jwt")
|
|
539
|
+
):
|
|
540
|
+
if not schemes:
|
|
541
|
+
schemes.append(
|
|
542
|
+
ExtractedAuthScheme(
|
|
543
|
+
scheme_type=AuthSchemeType.JWT_BEARER,
|
|
544
|
+
name="jwt_from_imports",
|
|
545
|
+
location=CodeLocation(file=parsed_file.path, line=0),
|
|
546
|
+
config={"detected_via": "import", "module": module},
|
|
547
|
+
confidence=Confidence.LOW,
|
|
548
|
+
)
|
|
549
|
+
)
|
|
550
|
+
break
|
|
551
|
+
|
|
552
|
+
return schemes
|
|
553
|
+
|
|
554
|
+
# -------------------------------------------------------------------------
|
|
555
|
+
# Auth dependency extraction
|
|
556
|
+
# -------------------------------------------------------------------------
|
|
557
|
+
|
|
558
|
+
def extract_auth_dependencies(
|
|
559
|
+
self,
|
|
560
|
+
parsed_file: ParsedFile,
|
|
561
|
+
known_scheme_names: set[str] | None = None,
|
|
562
|
+
**kwargs: Any,
|
|
563
|
+
) -> list[ExtractedAuthDependency]:
|
|
564
|
+
auth_deps: list[ExtractedAuthDependency] = []
|
|
565
|
+
|
|
566
|
+
for cls in parsed_file.classes:
|
|
567
|
+
class_security = [d for d in cls.decorators if d.name in _SECURITY_ANNOTATIONS]
|
|
568
|
+
class_roles = self._roles_from_annotations(class_security)
|
|
569
|
+
|
|
570
|
+
if class_security:
|
|
571
|
+
auth_deps.append(
|
|
572
|
+
ExtractedAuthDependency(
|
|
573
|
+
name=cls.name,
|
|
574
|
+
qualified_name=cls.qualified_name,
|
|
575
|
+
location=cls.location,
|
|
576
|
+
dependency_type=AuthDependencyType.ANNOTATION,
|
|
577
|
+
requires_roles=class_roles,
|
|
578
|
+
confidence=Confidence.HIGH,
|
|
579
|
+
)
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
for method in cls.methods:
|
|
583
|
+
method_security = [d for d in method.decorators if d.name in _SECURITY_ANNOTATIONS]
|
|
584
|
+
if method_security:
|
|
585
|
+
method_roles = self._roles_from_annotations(method_security)
|
|
586
|
+
auth_deps.append(
|
|
587
|
+
ExtractedAuthDependency(
|
|
588
|
+
name=f"{cls.name}.{method.name}",
|
|
589
|
+
qualified_name=method.qualified_name,
|
|
590
|
+
location=method.location,
|
|
591
|
+
dependency_type=AuthDependencyType.ANNOTATION,
|
|
592
|
+
requires_roles=class_roles + method_roles,
|
|
593
|
+
confidence=Confidence.HIGH,
|
|
594
|
+
)
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
# ContainerRequestFilter that looks auth-related
|
|
598
|
+
is_filter = any(b in _FILTER_BASE_TYPES for b in cls.base_classes)
|
|
599
|
+
if is_filter and any(kw in cls.name.lower() for kw in _AUTH_RELATED_NAMES):
|
|
600
|
+
auth_deps.append(
|
|
601
|
+
ExtractedAuthDependency(
|
|
602
|
+
name=cls.name,
|
|
603
|
+
qualified_name=cls.qualified_name,
|
|
604
|
+
location=cls.location,
|
|
605
|
+
dependency_type=AuthDependencyType.FILTER,
|
|
606
|
+
confidence=Confidence.MEDIUM,
|
|
607
|
+
)
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
return auth_deps
|
|
611
|
+
|
|
612
|
+
@staticmethod
|
|
613
|
+
def _roles_from_annotations(decorators: list[ParsedDecorator]) -> list[str]:
|
|
614
|
+
roles: list[str] = []
|
|
615
|
+
for dec in decorators:
|
|
616
|
+
if dec.name == "RolesAllowed":
|
|
617
|
+
for val in dec.positional_args:
|
|
618
|
+
if isinstance(val, list):
|
|
619
|
+
roles.extend(str(v) for v in val)
|
|
620
|
+
elif isinstance(val, str):
|
|
621
|
+
roles.append(val)
|
|
622
|
+
val = dec.arguments.get("value")
|
|
623
|
+
if val:
|
|
624
|
+
if isinstance(val, list):
|
|
625
|
+
roles.extend(str(v) for v in val)
|
|
626
|
+
elif isinstance(val, str):
|
|
627
|
+
roles.append(val)
|
|
628
|
+
return roles
|
|
629
|
+
|
|
630
|
+
# -------------------------------------------------------------------------
|
|
631
|
+
# Middleware extraction
|
|
632
|
+
# -------------------------------------------------------------------------
|
|
633
|
+
|
|
634
|
+
def extract_middleware(self, parsed_file: ParsedFile) -> list[ExtractedMiddleware]:
|
|
635
|
+
middleware: list[ExtractedMiddleware] = []
|
|
636
|
+
|
|
637
|
+
for cls in parsed_file.classes:
|
|
638
|
+
is_filter = any(b in _FILTER_BASE_TYPES for b in cls.base_classes)
|
|
639
|
+
if not is_filter:
|
|
640
|
+
# Also detect via @Provider + implement pattern (class name hint)
|
|
641
|
+
ann_names = {d.name for d in cls.decorators}
|
|
642
|
+
is_filter = "Provider" in ann_names and any(
|
|
643
|
+
kw in cls.name.lower() for kw in ("filter", "interceptor")
|
|
644
|
+
)
|
|
645
|
+
if not is_filter:
|
|
646
|
+
continue
|
|
647
|
+
|
|
648
|
+
ops = self._infer_filter_ops(cls)
|
|
649
|
+
middleware.append(
|
|
650
|
+
ExtractedMiddleware(
|
|
651
|
+
name=cls.name,
|
|
652
|
+
qualified_name=cls.qualified_name,
|
|
653
|
+
location=cls.location,
|
|
654
|
+
middleware_type="filter",
|
|
655
|
+
applies_to_all=True,
|
|
656
|
+
applies_to_patterns=[],
|
|
657
|
+
operations=ops,
|
|
658
|
+
confidence=Confidence.HIGH,
|
|
659
|
+
)
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
return middleware
|
|
663
|
+
|
|
664
|
+
@staticmethod
|
|
665
|
+
def _infer_filter_ops(cls: ParsedClass) -> list[str]:
|
|
666
|
+
label = cls.name.lower() + " " + " ".join(b.lower() for b in cls.base_classes)
|
|
667
|
+
ops: list[str] = []
|
|
668
|
+
if any(kw in label for kw in ["auth", "jwt", "token", "security", "login"]):
|
|
669
|
+
ops.append("auth")
|
|
670
|
+
if "cors" in label:
|
|
671
|
+
ops.append("cors")
|
|
672
|
+
if "log" in label:
|
|
673
|
+
ops.append("logging")
|
|
674
|
+
if "rate" in label or "throttl" in label:
|
|
675
|
+
ops.append("rate_limiting")
|
|
676
|
+
return ops or ["custom"]
|
|
677
|
+
|
|
678
|
+
# -------------------------------------------------------------------------
|
|
679
|
+
# Annotation helpers
|
|
680
|
+
# -------------------------------------------------------------------------
|
|
681
|
+
|
|
682
|
+
@staticmethod
|
|
683
|
+
def _find_annotation(decorators: list[ParsedDecorator], name: str) -> ParsedDecorator | None:
|
|
684
|
+
for dec in decorators:
|
|
685
|
+
if dec.name == name:
|
|
686
|
+
return dec
|
|
687
|
+
return None
|
|
688
|
+
|
|
689
|
+
def _annotation_path(self, dec: ParsedDecorator) -> str | None:
|
|
690
|
+
"""Extract the first path string from @Path("/foo") or @ApplicationPath("/api")."""
|
|
691
|
+
if dec.positional_args:
|
|
692
|
+
val = dec.positional_args[0]
|
|
693
|
+
if isinstance(val, str):
|
|
694
|
+
return val
|
|
695
|
+
val = dec.arguments.get("value")
|
|
696
|
+
if isinstance(val, str):
|
|
697
|
+
return val
|
|
698
|
+
return None
|
|
699
|
+
|
|
700
|
+
def _annotation_str_from_decorators(
|
|
701
|
+
self, decorators: list[ParsedDecorator], name: str
|
|
702
|
+
) -> str | None:
|
|
703
|
+
for dec in decorators:
|
|
704
|
+
if dec.name == name:
|
|
705
|
+
if dec.positional_args:
|
|
706
|
+
val = dec.positional_args[0]
|
|
707
|
+
if isinstance(val, str):
|
|
708
|
+
return val
|
|
709
|
+
if isinstance(val, list) and val:
|
|
710
|
+
return str(val[0])
|
|
711
|
+
val = dec.arguments.get("value")
|
|
712
|
+
if isinstance(val, str):
|
|
713
|
+
return val
|
|
714
|
+
if isinstance(val, list) and val:
|
|
715
|
+
return str(val[0])
|
|
716
|
+
return None
|
|
717
|
+
|
|
718
|
+
def _annotation_str_from_class(self, cls: ParsedClass, name: str) -> str | None:
|
|
719
|
+
return self._annotation_str_from_decorators(cls.decorators, name)
|
|
720
|
+
|
|
721
|
+
@staticmethod
|
|
722
|
+
def _join_paths(prefix: str, path: str) -> str:
|
|
723
|
+
prefix = prefix.rstrip("/")
|
|
724
|
+
if path and not path.startswith("/"):
|
|
725
|
+
path = "/" + path
|
|
726
|
+
result = prefix + path
|
|
727
|
+
return result if result else "/"
|
|
728
|
+
|
|
729
|
+
# -------------------------------------------------------------------------
|
|
730
|
+
# JWT config extraction
|
|
731
|
+
# -------------------------------------------------------------------------
|
|
732
|
+
|
|
733
|
+
_jwt_extractor = JavaJwtConfigExtractor()
|
|
734
|
+
|
|
735
|
+
def extract_jwt_config(
|
|
736
|
+
self,
|
|
737
|
+
parsed_file: ParsedFile,
|
|
738
|
+
context: AnalysisContext | None = None,
|
|
739
|
+
) -> ExtractedJwtConfig | None:
|
|
740
|
+
return self._jwt_extractor.extract(parsed_file)
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
# =============================================================================
|
|
744
|
+
# Registration
|
|
745
|
+
# =============================================================================
|
|
746
|
+
|
|
747
|
+
_jaxrs_plugin = JaxRsPlugin()
|
|
748
|
+
FrameworkPluginRegistry.register(_jaxrs_plugin)
|