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,381 @@
1
+ """
2
+ Fastify framework plugin.
3
+
4
+ Extracts HTTP routes registered via:
5
+ - Shorthand methods: fastify.get('/path', handler) or fastify.get('/path', opts, handler)
6
+ - Route object: fastify.route({ method: 'GET', url: '/path', handler })
7
+ - Method arrays: fastify.route({ method: ['GET', 'POST'], url: '/path', handler })
8
+ - Path parameters: /users/:id → /users/{id}
9
+ - Cross-file prefix from _url_prefix_map (populated by url_prefix_resolver which
10
+ handles both Express use() and Fastify register() mounts).
11
+
12
+ Auth detection covers @fastify/jwt, @fastify/bearer-auth, @fastify/auth, and
13
+ local jwt/auth utility modules imported into the file.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ import re
20
+ from typing import TYPE_CHECKING, Any
21
+
22
+ from ...core.types import (
23
+ AuthDependencyType,
24
+ AuthSchemeType,
25
+ CodeLocation,
26
+ Confidence,
27
+ Framework,
28
+ HttpMethod,
29
+ Language,
30
+ ParameterLocation,
31
+ QualifiedName,
32
+ )
33
+ from ...parsing.base import ParsedFile
34
+ from ..base import (
35
+ BaseFrameworkPlugin,
36
+ ExtractedAuthDependency,
37
+ ExtractedAuthScheme,
38
+ ExtractedDependency,
39
+ ExtractedMiddleware,
40
+ ExtractedParameter,
41
+ ExtractedRoute,
42
+ FrameworkPluginRegistry,
43
+ )
44
+
45
+ if TYPE_CHECKING:
46
+ from ...parsing.services import AnalysisContext
47
+
48
+ logger = logging.getLogger(__name__)
49
+
50
+ # Fastify shorthand HTTP methods (all lowercase)
51
+ _FASTIFY_HTTP_METHODS: frozenset[str] = frozenset(
52
+ {"get", "post", "put", "patch", "delete", "options", "head", "all"}
53
+ )
54
+
55
+ # Regex to convert Fastify :param → {param} (same convention as Express)
56
+ _PARAM_RE = re.compile(r":([A-Za-z_]\w*)")
57
+
58
+ # Regex to extract fields from a fastify.route({...}) object expression
59
+ _ROUTE_URL_RE = re.compile(r'\burl\s*:\s*[\'"]([^\'"]+)[\'"]')
60
+ _ROUTE_METHOD_STR_RE = re.compile(r'\bmethod\s*:\s*[\'"]([A-Z]+)[\'"]')
61
+ _ROUTE_METHOD_ARR_RE = re.compile(r"\bmethod\s*:\s*\[([^\]]+)\]")
62
+ _ROUTE_HANDLER_VAR_RE = re.compile(r"\bhandler\s*:\s*([A-Za-z_]\w*)\s*[,}\n]")
63
+
64
+ # JWT / auth import hints for auth-scheme detection
65
+ _JWT_PACKAGE_HINTS = ("@fastify/jwt", "fastify-jwt", "jsonwebtoken", "jwt-simple")
66
+ _BEARER_PACKAGE_HINTS = ("@fastify/bearer-auth", "fastify-bearer-auth")
67
+ _AUTH_MIDDLEWARE_HINTS = (
68
+ "auth",
69
+ "authentication",
70
+ "authorize",
71
+ "authorization",
72
+ "jwt",
73
+ "token",
74
+ "apikey",
75
+ "api_key",
76
+ "bearer",
77
+ "security",
78
+ "verify",
79
+ "guard",
80
+ )
81
+
82
+
83
+ def _colon_to_curly(path: str) -> str:
84
+ return _PARAM_RE.sub(r"{\1}", path)
85
+
86
+
87
+ def _normalize_path(path: str, prefix: str = "") -> str:
88
+ if prefix:
89
+ path = prefix.rstrip("/") + "/" + path.lstrip("/")
90
+ path = re.sub(r"/+", "/", path)
91
+ if not path.startswith("/"):
92
+ path = "/" + path
93
+ if path != "/" and path.endswith("/"):
94
+ path = path.rstrip("/")
95
+ return path
96
+
97
+
98
+ def _extract_path_params(raw_path: str) -> list[ExtractedParameter]:
99
+ return [
100
+ ExtractedParameter(name=m.group(1), location=ParameterLocation.PATH)
101
+ for m in _PARAM_RE.finditer(raw_path)
102
+ ]
103
+
104
+
105
+ def _method_from_str(method: str) -> HttpMethod:
106
+ return {
107
+ "get": HttpMethod.GET,
108
+ "post": HttpMethod.POST,
109
+ "put": HttpMethod.PUT,
110
+ "patch": HttpMethod.PATCH,
111
+ "delete": HttpMethod.DELETE,
112
+ "options": HttpMethod.OPTIONS,
113
+ "head": HttpMethod.HEAD,
114
+ "all": HttpMethod.GET,
115
+ }.get(method.lower(), HttpMethod.GET)
116
+
117
+
118
+ def _parse_method_array(arr_text: str) -> list[str]:
119
+ """Parse ['GET', 'POST'] text into a list of uppercase method strings."""
120
+ methods = []
121
+ for m in re.finditer(r"['\"]([A-Z]+)['\"]", arr_text):
122
+ methods.append(m.group(1))
123
+ return methods or ["GET"]
124
+
125
+
126
+ class FastifyPlugin(BaseFrameworkPlugin):
127
+ """
128
+ Framework plugin for Fastify.
129
+
130
+ Supports shorthand route methods, fastify.route() object registration,
131
+ method arrays, path parameters, and cross-file prefix resolution via
132
+ fastify.register(plugin, { prefix: '...' }).
133
+ """
134
+
135
+ FRAMEWORK = Framework.FASTIFY
136
+ LANGUAGE = Language.JAVASCRIPT
137
+ DETECTION_IMPORTS: frozenset[str] = frozenset({"fastify", "fastify-plugin"})
138
+
139
+ def detect(self, parsed_file: ParsedFile) -> bool:
140
+ for imp in parsed_file.imports:
141
+ mod = imp.module
142
+ if (
143
+ mod == "fastify"
144
+ or mod == "fastify-plugin"
145
+ or mod.startswith("@fastify/")
146
+ or mod.startswith("fastify/")
147
+ ):
148
+ return True
149
+ return False
150
+
151
+ def extract_routes(
152
+ self,
153
+ parsed_file: ParsedFile,
154
+ context: AnalysisContext | None = None,
155
+ ) -> list[ExtractedRoute]:
156
+ routes: list[ExtractedRoute] = []
157
+
158
+ file_prefix = ""
159
+ if context and context.language_services:
160
+ prefix_map = context.language_services.get("_url_prefix_map")
161
+ if prefix_map:
162
+ file_key = str(parsed_file.path.resolve())
163
+ prefixes = prefix_map.get(file_key, [])
164
+ if prefixes:
165
+ file_prefix = prefixes[0]
166
+
167
+ for call in parsed_file.call_sites:
168
+ if not call.is_method_call:
169
+ continue
170
+
171
+ callee = call.callee_name.lower()
172
+
173
+ if callee == "route":
174
+ routes.extend(self._extract_from_route_object(call, parsed_file, file_prefix))
175
+ elif callee in _FASTIFY_HTTP_METHODS:
176
+ route = self._extract_from_shorthand(call, callee, parsed_file, file_prefix)
177
+ if route:
178
+ routes.append(route)
179
+
180
+ return routes
181
+
182
+ def _extract_from_shorthand(
183
+ self, call: Any, method_str: str, parsed_file: ParsedFile, file_prefix: str
184
+ ) -> ExtractedRoute | None:
185
+ """Handle fastify.get('/path', handler) and fastify.get('/path', opts, handler)."""
186
+ if not call.arguments:
187
+ return None
188
+
189
+ path_arg = call.arguments[0]
190
+ if not path_arg.is_literal or not isinstance(path_arg.literal_value, str):
191
+ return None
192
+
193
+ raw_path = path_arg.literal_value
194
+ path = _colon_to_curly(raw_path)
195
+ path = _normalize_path(path, file_prefix)
196
+
197
+ # Handler: 2-arg form → args[1]; 3-arg form → args[2] (middle arg is opts)
198
+ handler_name = "<anonymous>"
199
+ handler_idx = 1 if len(call.arguments) < 3 else 2
200
+ if len(call.arguments) > handler_idx:
201
+ h = call.arguments[handler_idx]
202
+ if h.is_variable and h.variable_name:
203
+ handler_name = h.variable_name
204
+ elif h.is_expression:
205
+ handler_name = "<arrow>"
206
+
207
+ return ExtractedRoute(
208
+ method=_method_from_str(method_str),
209
+ path=path,
210
+ handler_function=QualifiedName(module=parsed_file.path.stem, name=handler_name),
211
+ handler_location=call.location,
212
+ path_params=_extract_path_params(raw_path),
213
+ )
214
+
215
+ def _extract_from_route_object(
216
+ self, call: Any, parsed_file: ParsedFile, file_prefix: str
217
+ ) -> list[ExtractedRoute]:
218
+ """Handle fastify.route({ method, url, handler }) — possibly method arrays."""
219
+ if not call.arguments:
220
+ return []
221
+
222
+ obj_arg = call.arguments[0]
223
+ if not obj_arg.is_expression or not obj_arg.expression_text:
224
+ return []
225
+
226
+ text = obj_arg.expression_text
227
+
228
+ url_m = _ROUTE_URL_RE.search(text)
229
+ if not url_m:
230
+ return []
231
+
232
+ raw_path = url_m.group(1)
233
+ path = _colon_to_curly(raw_path)
234
+ path = _normalize_path(path, file_prefix)
235
+ path_params = _extract_path_params(raw_path)
236
+
237
+ handler_m = _ROUTE_HANDLER_VAR_RE.search(text)
238
+ handler_name = handler_m.group(1) if handler_m else "<anonymous>"
239
+
240
+ # Collect HTTP methods — string or array form
241
+ methods: list[str] = []
242
+ arr_m = _ROUTE_METHOD_ARR_RE.search(text)
243
+ if arr_m:
244
+ methods = _parse_method_array(arr_m.group(1))
245
+ else:
246
+ str_m = _ROUTE_METHOD_STR_RE.search(text)
247
+ if str_m:
248
+ methods = [str_m.group(1)]
249
+
250
+ if not methods:
251
+ methods = ["GET"]
252
+
253
+ return [
254
+ ExtractedRoute(
255
+ method=_method_from_str(m),
256
+ path=path,
257
+ handler_function=QualifiedName(module=parsed_file.path.stem, name=handler_name),
258
+ handler_location=call.location,
259
+ path_params=path_params,
260
+ )
261
+ for m in methods
262
+ ]
263
+
264
+ def extract_dependencies(self, parsed_file: ParsedFile) -> list[ExtractedDependency]:
265
+ return []
266
+
267
+ def extract_auth_schemes(self, parsed_file: ParsedFile) -> list[ExtractedAuthScheme]:
268
+ schemes: list[ExtractedAuthScheme] = []
269
+ seen: set[str] = set()
270
+ file_loc = CodeLocation(file=parsed_file.path, line=1)
271
+
272
+ def _add(name: str, scheme_type: AuthSchemeType) -> None:
273
+ if name not in seen:
274
+ seen.add(name)
275
+ schemes.append(
276
+ ExtractedAuthScheme(
277
+ scheme_type=scheme_type,
278
+ name=name,
279
+ location=file_loc,
280
+ confidence=Confidence.HIGH,
281
+ )
282
+ )
283
+
284
+ for imp in parsed_file.imports:
285
+ mod = imp.module
286
+ mod_lower = mod.lower()
287
+ if any(hint in mod_lower for hint in _JWT_PACKAGE_HINTS):
288
+ _add("JwtBearer", AuthSchemeType.JWT_BEARER)
289
+ elif any(hint in mod_lower for hint in _BEARER_PACKAGE_HINTS):
290
+ _add("BearerAuth", AuthSchemeType.API_KEY_HEADER)
291
+ elif mod == "@fastify/auth" or mod == "fastify-auth":
292
+ _add("FastifyAuth", AuthSchemeType.JWT_BEARER)
293
+ elif mod.startswith((".", "/")):
294
+ stem = mod_lower.rsplit("/", 1)[-1]
295
+ if any(h in stem for h in ("jwt", "authentication", "auth")):
296
+ _add("JwtBearer", AuthSchemeType.JWT_BEARER)
297
+ elif any(h in stem for h in ("apikey", "api-key", "api_key")):
298
+ _add("ApiKey", AuthSchemeType.API_KEY_HEADER)
299
+
300
+ for call in parsed_file.call_sites:
301
+ if call.callee_name.lower() in ("verify", "validate", "decode") and call.is_method_call:
302
+ _add("JwtBearer", AuthSchemeType.JWT_BEARER)
303
+
304
+ return schemes
305
+
306
+ def extract_auth_dependencies(
307
+ self,
308
+ parsed_file: ParsedFile,
309
+ known_scheme_names: set[str] | None = None,
310
+ **kwargs: Any,
311
+ ) -> list[ExtractedAuthDependency]:
312
+ """
313
+ Detect Fastify auth hooks applied via fastify.addHook('onRequest', authMiddleware)
314
+ or fastify.register(authPlugin).
315
+ """
316
+ deps: list[ExtractedAuthDependency] = []
317
+
318
+ auth_imports: set[str] = set()
319
+ for imp in parsed_file.imports:
320
+ mod_lower = imp.module.lower()
321
+ is_auth_module = any(h in mod_lower for h in _AUTH_MIDDLEWARE_HINTS)
322
+ for name in imp.names:
323
+ if is_auth_module or any(h in name.lower() for h in _AUTH_MIDDLEWARE_HINTS):
324
+ auth_imports.add(name)
325
+
326
+ for call in parsed_file.call_sites:
327
+ if not call.is_method_call:
328
+ continue
329
+ callee = call.callee_name.lower()
330
+ if callee not in ("addhook", "register"):
331
+ continue
332
+ if not call.arguments:
333
+ continue
334
+
335
+ middleware_names: list[str] = []
336
+ requires_roles: list[str] = []
337
+
338
+ for arg in call.arguments:
339
+ if arg.is_variable and arg.variable_name:
340
+ vname = arg.variable_name
341
+ if (
342
+ any(h in vname.lower() for h in _AUTH_MIDDLEWARE_HINTS)
343
+ or vname in auth_imports
344
+ ):
345
+ middleware_names.append(vname)
346
+
347
+ if not middleware_names:
348
+ continue
349
+
350
+ uses_schemes: list[str] = []
351
+ for name in middleware_names:
352
+ low = name.lower()
353
+ if any(h in low for h in ("jwt", "authentication", "bearer", "token")):
354
+ if "JwtBearer" not in uses_schemes:
355
+ uses_schemes.append("JwtBearer")
356
+ elif any(h in low for h in ("apikey", "api_key", "api-key")):
357
+ if "ApiKey" not in uses_schemes:
358
+ uses_schemes.append("ApiKey")
359
+
360
+ loc = call.location or CodeLocation(file=parsed_file.path, line=1)
361
+ dep_name = "+".join(middleware_names)
362
+ deps.append(
363
+ ExtractedAuthDependency(
364
+ name=dep_name,
365
+ qualified_name=QualifiedName(module=parsed_file.path.stem, name=dep_name),
366
+ location=loc,
367
+ dependency_type=AuthDependencyType.MIDDLEWARE,
368
+ uses_schemes=uses_schemes,
369
+ requires_roles=requires_roles,
370
+ confidence=Confidence.HIGH,
371
+ )
372
+ )
373
+
374
+ return deps
375
+
376
+ def extract_middleware(self, parsed_file: ParsedFile) -> list[ExtractedMiddleware]:
377
+ return []
378
+
379
+
380
+ _fastify_plugin = FastifyPlugin()
381
+ FrameworkPluginRegistry.register(_fastify_plugin)
@@ -0,0 +1,198 @@
1
+ """
2
+ JavaScript/TypeScript GraphQL framework plugin.
3
+
4
+ Supports:
5
+ - NestJS GraphQL (@nestjs/graphql): @Resolver classes with @Query/@Mutation/@Subscription
6
+ - TypeGraphQL (type-graphql): same @Resolver/@Query/@Mutation decorator pattern
7
+
8
+ Path format: /graphql:Query.methodName (GET)
9
+ /graphql:Mutation.methodName (POST)
10
+ /graphql:Subscription.methodName (POST)
11
+
12
+ Apollo Server standalone (resolver maps) is not yet supported — it requires
13
+ object-literal analysis rather than decorator extraction.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ from typing import TYPE_CHECKING, Any
20
+
21
+ from ...core.types import (
22
+ Framework,
23
+ HttpMethod,
24
+ Language,
25
+ QualifiedName,
26
+ )
27
+ from ...parsing.base import ParsedDecorator, ParsedFile
28
+ from ..base import (
29
+ BaseFrameworkPlugin,
30
+ ExtractedAuthDependency,
31
+ ExtractedAuthScheme,
32
+ ExtractedDependency,
33
+ ExtractedMiddleware,
34
+ ExtractedRoute,
35
+ FrameworkPluginRegistry,
36
+ )
37
+
38
+ if TYPE_CHECKING:
39
+ from ...parsing.services import AnalysisContext
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+ # Imports that identify a NestJS GraphQL or TypeGraphQL file
44
+ _GRAPHQL_IMPORTS: frozenset[str] = frozenset(
45
+ {
46
+ "@nestjs/graphql",
47
+ "type-graphql",
48
+ }
49
+ )
50
+
51
+ # Decorator names that mark a GraphQL operation on a method
52
+ _QUERY_DECORATORS: frozenset[str] = frozenset({"Query"})
53
+ _MUTATION_DECORATORS: frozenset[str] = frozenset({"Mutation"})
54
+ _SUBSCRIPTION_DECORATORS: frozenset[str] = frozenset({"Subscription"})
55
+
56
+ # Auth-related decorators
57
+ _AUTH_DECORATORS: frozenset[str] = frozenset(
58
+ {
59
+ "UseGuards",
60
+ "Roles",
61
+ "Public",
62
+ "ApiBearerAuth",
63
+ "ApiSecurity",
64
+ "Auth",
65
+ "Authorize",
66
+ }
67
+ )
68
+
69
+
70
+ def _gql_path(operation_type: str, field_name: str) -> str:
71
+ """Internal path token for a GraphQL operation: /graphql:<OperationType>.<fieldName>.
72
+
73
+ Not a real HTTP URL — downstream consumers translate to POST /graphql with
74
+ the appropriate query/mutation body. Query → GET; Mutation/Subscription → POST.
75
+ """
76
+ return f"/graphql:{operation_type}.{field_name}"
77
+
78
+
79
+ def _gql_http_method(operation_type: str) -> HttpMethod:
80
+ if operation_type in ("Mutation", "Subscription"):
81
+ return HttpMethod.POST
82
+ return HttpMethod.GET
83
+
84
+
85
+ def _find_decorator(
86
+ decorators: list[ParsedDecorator], names: frozenset[str]
87
+ ) -> ParsedDecorator | None:
88
+ for dec in decorators:
89
+ if dec.name in names:
90
+ return dec
91
+ return None
92
+
93
+
94
+ class JavaScriptGraphQLPlugin(BaseFrameworkPlugin):
95
+ """
96
+ Framework plugin for JavaScript/TypeScript GraphQL (NestJS GraphQL + TypeGraphQL).
97
+
98
+ Detects @Resolver classes and extracts @Query/@Mutation/@Subscription methods
99
+ as GraphQL operations using the /graphql:Operation.field path convention.
100
+ """
101
+
102
+ FRAMEWORK = Framework.GRAPHQL
103
+ LANGUAGE = Language.JAVASCRIPT
104
+ DETECTION_IMPORTS: frozenset[str] = _GRAPHQL_IMPORTS
105
+
106
+ def detect(self, parsed_file: ParsedFile) -> bool:
107
+ return any(imp.module in _GRAPHQL_IMPORTS for imp in parsed_file.imports)
108
+
109
+ def extract_routes(
110
+ self,
111
+ parsed_file: ParsedFile,
112
+ context: AnalysisContext | None = None,
113
+ ) -> list[ExtractedRoute]:
114
+ routes: list[ExtractedRoute] = []
115
+
116
+ for cls in parsed_file.classes:
117
+ # Must have @Resolver decorator
118
+ if not _find_decorator(cls.decorators, frozenset({"Resolver"})):
119
+ continue
120
+
121
+ for method in cls.methods:
122
+ operation_type: str | None = None
123
+ if _find_decorator(method.decorators, _QUERY_DECORATORS):
124
+ operation_type = "Query"
125
+ elif _find_decorator(method.decorators, _MUTATION_DECORATORS):
126
+ operation_type = "Mutation"
127
+ elif _find_decorator(method.decorators, _SUBSCRIPTION_DECORATORS):
128
+ operation_type = "Subscription"
129
+
130
+ if operation_type is None:
131
+ continue
132
+
133
+ # Operation name: decorator string arg takes precedence over method name.
134
+ # Schema-first style: @Query('posts') → field_name = "posts"
135
+ # Code-first style: @Query(() => Post) → no string arg, use method name
136
+ op_dec = _find_decorator(
137
+ method.decorators,
138
+ _QUERY_DECORATORS | _MUTATION_DECORATORS | _SUBSCRIPTION_DECORATORS,
139
+ )
140
+ raw_name = (
141
+ str(op_dec.positional_args[0]).strip("'\"")
142
+ if op_dec
143
+ and op_dec.positional_args
144
+ and isinstance(op_dec.positional_args[0], str)
145
+ and not op_dec.positional_args[0].startswith("(")
146
+ else None
147
+ )
148
+ field_name = raw_name if raw_name else method.name
149
+
150
+ # Auth guard
151
+ auth_guard: str | None = None
152
+ for dec in method.decorators + cls.decorators:
153
+ if dec.name in _AUTH_DECORATORS:
154
+ auth_guard = (
155
+ str(dec.positional_args[0]) if dec.positional_args else dec.name
156
+ )
157
+ break
158
+
159
+ path = _gql_path(operation_type, field_name)
160
+ routes.append(
161
+ ExtractedRoute(
162
+ method=_gql_http_method(operation_type),
163
+ path=path,
164
+ handler_function=QualifiedName(
165
+ module=parsed_file.path.stem,
166
+ name=f"{cls.name}.{method.name}",
167
+ ),
168
+ handler_location=method.location,
169
+ router_name=auth_guard,
170
+ kind="http",
171
+ )
172
+ )
173
+
174
+ return routes
175
+
176
+ def extract_dependencies(self, parsed_file: ParsedFile) -> list[ExtractedDependency]:
177
+ return []
178
+
179
+ def extract_auth_schemes(self, parsed_file: ParsedFile) -> list[ExtractedAuthScheme]:
180
+ from ..auth_helpers import extract_nestjs_auth_schemes
181
+
182
+ return extract_nestjs_auth_schemes(parsed_file)
183
+
184
+ def extract_auth_dependencies(
185
+ self,
186
+ parsed_file: ParsedFile,
187
+ known_scheme_names: set[str] | None = None,
188
+ **kwargs: Any,
189
+ ) -> list[ExtractedAuthDependency]:
190
+ from ..auth_helpers import extract_nestjs_auth_dependencies
191
+
192
+ return extract_nestjs_auth_dependencies(parsed_file)
193
+
194
+ def extract_middleware(self, parsed_file: ParsedFile) -> list[ExtractedMiddleware]:
195
+ return []
196
+
197
+
198
+ FrameworkPluginRegistry.register(JavaScriptGraphQLPlugin())