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,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())
|