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.
Files changed (111) hide show
  1. apisec_code_bolt/__init__.py +42 -0
  2. apisec_code_bolt/__main__.py +11 -0
  3. apisec_code_bolt/analysis/__init__.py +96 -0
  4. apisec_code_bolt/analysis/analyzer.py +2309 -0
  5. apisec_code_bolt/analysis/binding_tracker.py +341 -0
  6. apisec_code_bolt/analysis/call_graph.py +1197 -0
  7. apisec_code_bolt/analysis/call_graph_types.py +332 -0
  8. apisec_code_bolt/analysis/call_resolver.py +988 -0
  9. apisec_code_bolt/analysis/capability_tagger.py +322 -0
  10. apisec_code_bolt/analysis/config_scanner.py +197 -0
  11. apisec_code_bolt/analysis/data_flow.py +1883 -0
  12. apisec_code_bolt/analysis/dependency_extractor.py +959 -0
  13. apisec_code_bolt/analysis/flow_analysis.py +1406 -0
  14. apisec_code_bolt/analysis/hof_catalog.py +61 -0
  15. apisec_code_bolt/analysis/integration_detector.py +1399 -0
  16. apisec_code_bolt/analysis/literal_scanner.py +300 -0
  17. apisec_code_bolt/analysis/path_normalizer.py +55 -0
  18. apisec_code_bolt/analysis/read_site_detector.py +310 -0
  19. apisec_code_bolt/analysis/request_patterns.py +162 -0
  20. apisec_code_bolt/analysis/sensitivity_classifier.py +224 -0
  21. apisec_code_bolt/analysis/sink_evidence.py +333 -0
  22. apisec_code_bolt/analysis/url_prefix_resolver.py +338 -0
  23. apisec_code_bolt/cli/__init__.py +5 -0
  24. apisec_code_bolt/cli/exit_codes.py +17 -0
  25. apisec_code_bolt/cli/main.py +1069 -0
  26. apisec_code_bolt/cloud/__init__.py +1 -0
  27. apisec_code_bolt/cloud/apisec_client.py +118 -0
  28. apisec_code_bolt/cloud/client.py +255 -0
  29. apisec_code_bolt/core/__init__.py +75 -0
  30. apisec_code_bolt/core/config.py +528 -0
  31. apisec_code_bolt/core/credentials.py +65 -0
  32. apisec_code_bolt/core/discovery.py +433 -0
  33. apisec_code_bolt/core/log_format.py +115 -0
  34. apisec_code_bolt/core/manifest.py +1009 -0
  35. apisec_code_bolt/core/repo.py +280 -0
  36. apisec_code_bolt/core/state.py +59 -0
  37. apisec_code_bolt/core/telemetry.py +451 -0
  38. apisec_code_bolt/core/types.py +587 -0
  39. apisec_code_bolt/fingerprinting/__init__.py +1 -0
  40. apisec_code_bolt/frameworks/__init__.py +29 -0
  41. apisec_code_bolt/frameworks/_jwt_common.py +50 -0
  42. apisec_code_bolt/frameworks/auth_helpers.py +437 -0
  43. apisec_code_bolt/frameworks/base.py +608 -0
  44. apisec_code_bolt/frameworks/dotnet/__init__.py +17 -0
  45. apisec_code_bolt/frameworks/dotnet/_path_helpers.py +43 -0
  46. apisec_code_bolt/frameworks/dotnet/aspnet_plugin.py +2546 -0
  47. apisec_code_bolt/frameworks/dotnet/grpc_plugin.py +559 -0
  48. apisec_code_bolt/frameworks/dotnet/jwt_config_extractor.py +545 -0
  49. apisec_code_bolt/frameworks/dotnet/legacy_aspnet_plugin.py +732 -0
  50. apisec_code_bolt/frameworks/dotnet/refit_plugin.py +374 -0
  51. apisec_code_bolt/frameworks/dotnet/wcf_plugin.py +1239 -0
  52. apisec_code_bolt/frameworks/java/__init__.py +6 -0
  53. apisec_code_bolt/frameworks/java/_annotations.py +167 -0
  54. apisec_code_bolt/frameworks/java/_constraints.py +128 -0
  55. apisec_code_bolt/frameworks/java/graphql_plugin.py +287 -0
  56. apisec_code_bolt/frameworks/java/jaxrs_plugin.py +748 -0
  57. apisec_code_bolt/frameworks/java/jwt_config_extractor.py +361 -0
  58. apisec_code_bolt/frameworks/java/micronaut_plugin.py +1059 -0
  59. apisec_code_bolt/frameworks/java/spring_plugin.py +1293 -0
  60. apisec_code_bolt/frameworks/js/__init__.py +8 -0
  61. apisec_code_bolt/frameworks/js/express_plugin.py +391 -0
  62. apisec_code_bolt/frameworks/js/fastify_plugin.py +381 -0
  63. apisec_code_bolt/frameworks/js/graphql_plugin.py +198 -0
  64. apisec_code_bolt/frameworks/js/nestjs_plugin.py +423 -0
  65. apisec_code_bolt/frameworks/python/__init__.py +19 -0
  66. apisec_code_bolt/frameworks/python/celery_plugin.py +393 -0
  67. apisec_code_bolt/frameworks/python/click_plugin.py +427 -0
  68. apisec_code_bolt/frameworks/python/django_plugin.py +867 -0
  69. apisec_code_bolt/frameworks/python/fastapi/__init__.py +28 -0
  70. apisec_code_bolt/frameworks/python/fastapi/plugin.py +1390 -0
  71. apisec_code_bolt/frameworks/python/flask_plugin.py +205 -0
  72. apisec_code_bolt/frameworks/python/graphql_plugin.py +274 -0
  73. apisec_code_bolt/frameworks/python/prefect_plugin.py +251 -0
  74. apisec_code_bolt/frameworks/python/webhook_plugin.py +255 -0
  75. apisec_code_bolt/parsing/__init__.py +62 -0
  76. apisec_code_bolt/parsing/base.py +554 -0
  77. apisec_code_bolt/parsing/csharp/__init__.py +5 -0
  78. apisec_code_bolt/parsing/csharp/language_services.py +203 -0
  79. apisec_code_bolt/parsing/csharp/literals.py +72 -0
  80. apisec_code_bolt/parsing/csharp/parser.py +1158 -0
  81. apisec_code_bolt/parsing/csharp/type_resolver.py +568 -0
  82. apisec_code_bolt/parsing/js/__init__.py +5 -0
  83. apisec_code_bolt/parsing/js/language_services.py +118 -0
  84. apisec_code_bolt/parsing/js/parser.py +622 -0
  85. apisec_code_bolt/parsing/jvm/__init__.py +7 -0
  86. apisec_code_bolt/parsing/jvm/language_services.py +270 -0
  87. apisec_code_bolt/parsing/jvm/parser.py +774 -0
  88. apisec_code_bolt/parsing/jvm/type_resolver.py +422 -0
  89. apisec_code_bolt/parsing/python/__init__.py +150 -0
  90. apisec_code_bolt/parsing/python/cbv_extractor.py +606 -0
  91. apisec_code_bolt/parsing/python/constant_resolver.py +500 -0
  92. apisec_code_bolt/parsing/python/cross_file_resolver.py +1054 -0
  93. apisec_code_bolt/parsing/python/dynamic_route_detector.py +532 -0
  94. apisec_code_bolt/parsing/python/expression_utils.py +221 -0
  95. apisec_code_bolt/parsing/python/extraction_types.py +271 -0
  96. apisec_code_bolt/parsing/python/language_services.py +487 -0
  97. apisec_code_bolt/parsing/python/parameter_analyzer.py +789 -0
  98. apisec_code_bolt/parsing/python/parser.py +719 -0
  99. apisec_code_bolt/parsing/python/path_resolver.py +576 -0
  100. apisec_code_bolt/parsing/python/router_registry.py +806 -0
  101. apisec_code_bolt/parsing/python/type_resolver.py +730 -0
  102. apisec_code_bolt/parsing/python/visitors.py +1544 -0
  103. apisec_code_bolt/parsing/services.py +544 -0
  104. apisec_code_bolt/query/__init__.py +1 -0
  105. apisec_code_bolt/query/ast_cache.py +182 -0
  106. apisec_code_bolt/query/executor.py +283 -0
  107. apisec_code_bolt/query/handlers.py +832 -0
  108. apisec_code_bolt-0.1.0.dist-info/METADATA +230 -0
  109. apisec_code_bolt-0.1.0.dist-info/RECORD +111 -0
  110. apisec_code_bolt-0.1.0.dist-info/WHEEL +4 -0
  111. 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)