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,423 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NestJS framework plugin.
|
|
3
|
+
|
|
4
|
+
Extracts HTTP routes from NestJS @Controller + @Get/@Post/@Put/@Patch/@Delete
|
|
5
|
+
decorator patterns. Supports:
|
|
6
|
+
- @Controller('prefix') and @Controller({ path: 'prefix' })
|
|
7
|
+
- Verb decorators: @Get, @Post, @Put, @Patch, @Delete, @Options, @Head, @All
|
|
8
|
+
- Path params: :id → {id}
|
|
9
|
+
- Auth: @UseGuards, @ApiBearerAuth, etc.
|
|
10
|
+
- Versioned controllers
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import re
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
|
|
19
|
+
from ...core.types import (
|
|
20
|
+
AuthDependencyType,
|
|
21
|
+
AuthSchemeType,
|
|
22
|
+
CodeLocation,
|
|
23
|
+
Confidence,
|
|
24
|
+
Framework,
|
|
25
|
+
HttpMethod,
|
|
26
|
+
Language,
|
|
27
|
+
ParameterLocation,
|
|
28
|
+
QualifiedName,
|
|
29
|
+
)
|
|
30
|
+
from ...parsing.base import ParsedDecorator, ParsedFile
|
|
31
|
+
from ..base import (
|
|
32
|
+
BaseFrameworkPlugin,
|
|
33
|
+
ExtractedAuthDependency,
|
|
34
|
+
ExtractedAuthScheme,
|
|
35
|
+
ExtractedDependency,
|
|
36
|
+
ExtractedMiddleware,
|
|
37
|
+
ExtractedParameter,
|
|
38
|
+
ExtractedRoute,
|
|
39
|
+
FrameworkPluginRegistry,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if TYPE_CHECKING:
|
|
43
|
+
from ...parsing.services import AnalysisContext
|
|
44
|
+
|
|
45
|
+
logger = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
# NestJS HTTP verb decorators → HttpMethod
|
|
48
|
+
_VERB_DECORATORS: dict[str, HttpMethod] = {
|
|
49
|
+
"Get": HttpMethod.GET,
|
|
50
|
+
"Post": HttpMethod.POST,
|
|
51
|
+
"Put": HttpMethod.PUT,
|
|
52
|
+
"Patch": HttpMethod.PATCH,
|
|
53
|
+
"Delete": HttpMethod.DELETE,
|
|
54
|
+
"Options": HttpMethod.OPTIONS,
|
|
55
|
+
"Head": HttpMethod.HEAD,
|
|
56
|
+
"All": HttpMethod.GET,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Auth-related decorators
|
|
60
|
+
_AUTH_DECORATORS: frozenset[str] = frozenset(
|
|
61
|
+
{
|
|
62
|
+
"UseGuards",
|
|
63
|
+
"Roles",
|
|
64
|
+
"Public",
|
|
65
|
+
"ApiBearerAuth",
|
|
66
|
+
"ApiSecurity",
|
|
67
|
+
"Auth",
|
|
68
|
+
"Authorize",
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Regex to convert NestJS :param to {param}
|
|
73
|
+
_PARAM_RE = re.compile(r":([A-Za-z_]\w*)")
|
|
74
|
+
# Regex to extract 'path' from object literal { path: 'xxx' }
|
|
75
|
+
_OBJ_PATH_RE = re.compile(r"path\s*:\s*['\"]([^'\"]+)['\"]")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _colon_to_curly(path: str) -> str:
|
|
79
|
+
return _PARAM_RE.sub(r"{\1}", path)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _extract_path_params(path: str) -> list[ExtractedParameter]:
|
|
83
|
+
return [
|
|
84
|
+
ExtractedParameter(name=m.group(1), location=ParameterLocation.PATH)
|
|
85
|
+
for m in _PARAM_RE.finditer(path)
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _get_decorator_path(dec: ParsedDecorator) -> str:
|
|
90
|
+
"""Extract the path string from a decorator (string arg or { path: '...' } object)."""
|
|
91
|
+
if not dec.positional_args:
|
|
92
|
+
return ""
|
|
93
|
+
|
|
94
|
+
value = dec.positional_args[0]
|
|
95
|
+
if not isinstance(value, str):
|
|
96
|
+
return ""
|
|
97
|
+
|
|
98
|
+
# Check if it looks like an object literal
|
|
99
|
+
if value.strip().startswith("{"):
|
|
100
|
+
m = _OBJ_PATH_RE.search(value)
|
|
101
|
+
if m:
|
|
102
|
+
return m.group(1)
|
|
103
|
+
return ""
|
|
104
|
+
|
|
105
|
+
return value
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class NestJSPlugin(BaseFrameworkPlugin):
|
|
109
|
+
"""
|
|
110
|
+
Framework plugin for NestJS.
|
|
111
|
+
|
|
112
|
+
Detects NestJS usage via @nestjs/* imports and extracts routes
|
|
113
|
+
from @Controller + @Get/@Post etc. decorator patterns.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
FRAMEWORK = Framework.NESTJS
|
|
117
|
+
LANGUAGE = Language.JAVASCRIPT
|
|
118
|
+
DETECTION_IMPORTS: frozenset[str] = frozenset(
|
|
119
|
+
{
|
|
120
|
+
"@nestjs/common",
|
|
121
|
+
"@nestjs/core",
|
|
122
|
+
"@nestjs/swagger",
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def detect(self, parsed_file: ParsedFile) -> bool:
|
|
127
|
+
"""Detect NestJS by looking for @nestjs/* imports."""
|
|
128
|
+
return any(imp.module.startswith("@nestjs/") for imp in parsed_file.imports)
|
|
129
|
+
|
|
130
|
+
def extract_routes(
|
|
131
|
+
self,
|
|
132
|
+
parsed_file: ParsedFile,
|
|
133
|
+
context: AnalysisContext | None = None,
|
|
134
|
+
) -> list[ExtractedRoute]:
|
|
135
|
+
"""Extract routes from @Controller + @Get/@Post etc. patterns."""
|
|
136
|
+
routes: list[ExtractedRoute] = []
|
|
137
|
+
|
|
138
|
+
for cls in parsed_file.classes:
|
|
139
|
+
# Find @Controller decorator
|
|
140
|
+
controller_dec = _find_decorator(cls.decorators, "Controller")
|
|
141
|
+
if controller_dec is None:
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
# Extract controller prefix
|
|
145
|
+
prefix = _get_decorator_path(controller_dec)
|
|
146
|
+
|
|
147
|
+
# Process each method
|
|
148
|
+
for method in cls.methods:
|
|
149
|
+
# Find a verb decorator
|
|
150
|
+
verb_dec = _find_verb_decorator(method.decorators)
|
|
151
|
+
if verb_dec is None:
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
http_method = _VERB_DECORATORS[verb_dec.name]
|
|
155
|
+
|
|
156
|
+
# Get route path from decorator
|
|
157
|
+
method_path = _get_decorator_path(verb_dec)
|
|
158
|
+
|
|
159
|
+
# Build full path
|
|
160
|
+
if prefix and method_path:
|
|
161
|
+
full_path = prefix.rstrip("/") + "/" + method_path.lstrip("/")
|
|
162
|
+
elif prefix:
|
|
163
|
+
full_path = "/" + prefix.lstrip("/")
|
|
164
|
+
elif method_path:
|
|
165
|
+
full_path = "/" + method_path.lstrip("/")
|
|
166
|
+
else:
|
|
167
|
+
full_path = "/"
|
|
168
|
+
|
|
169
|
+
# Convert :param to {param}
|
|
170
|
+
full_path = _colon_to_curly(full_path)
|
|
171
|
+
if not full_path.startswith("/"):
|
|
172
|
+
full_path = "/" + full_path
|
|
173
|
+
full_path = re.sub(r"/+", "/", full_path)
|
|
174
|
+
|
|
175
|
+
# Extract path params from combined path
|
|
176
|
+
path_params = _extract_path_params(prefix + "/" + method_path)
|
|
177
|
+
|
|
178
|
+
# Auth: check for @UseGuards or similar
|
|
179
|
+
auth_guard: str | None = None
|
|
180
|
+
for auth_dec in method.decorators + cls.decorators:
|
|
181
|
+
if auth_dec.name in _AUTH_DECORATORS:
|
|
182
|
+
guard_val = (
|
|
183
|
+
auth_dec.positional_args[0]
|
|
184
|
+
if auth_dec.positional_args
|
|
185
|
+
else auth_dec.name
|
|
186
|
+
)
|
|
187
|
+
auth_guard = str(guard_val)
|
|
188
|
+
break
|
|
189
|
+
|
|
190
|
+
# Handler name: ClassName.methodName
|
|
191
|
+
handler_name = f"{cls.name}.{method.name}"
|
|
192
|
+
|
|
193
|
+
# Use decorator's location (line number) for benchmark accuracy
|
|
194
|
+
handler_location = verb_dec.location or method.location
|
|
195
|
+
|
|
196
|
+
routes.append(
|
|
197
|
+
ExtractedRoute(
|
|
198
|
+
method=http_method,
|
|
199
|
+
path=full_path,
|
|
200
|
+
handler_function=QualifiedName(
|
|
201
|
+
module=parsed_file.path.stem,
|
|
202
|
+
name=handler_name,
|
|
203
|
+
),
|
|
204
|
+
handler_location=handler_location,
|
|
205
|
+
path_params=path_params,
|
|
206
|
+
router_name=auth_guard,
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return routes
|
|
211
|
+
|
|
212
|
+
def extract_dependencies(self, parsed_file: ParsedFile) -> list[ExtractedDependency]:
|
|
213
|
+
return []
|
|
214
|
+
|
|
215
|
+
def extract_auth_schemes(self, parsed_file: ParsedFile) -> list[ExtractedAuthScheme]:
|
|
216
|
+
"""
|
|
217
|
+
Detect NestJS auth scheme definitions.
|
|
218
|
+
|
|
219
|
+
Covers:
|
|
220
|
+
- @ApiBearerAuth() import/usage → JWT_BEARER scheme
|
|
221
|
+
- AuthGuard('strategy') references → named Passport strategies
|
|
222
|
+
- Passport strategy class definitions (extends PassportStrategy)
|
|
223
|
+
"""
|
|
224
|
+
schemes: list[ExtractedAuthScheme] = []
|
|
225
|
+
seen_names: set[str] = set()
|
|
226
|
+
|
|
227
|
+
def _add(name: str, scheme_type: AuthSchemeType, line: int) -> None:
|
|
228
|
+
if name not in seen_names:
|
|
229
|
+
seen_names.add(name)
|
|
230
|
+
schemes.append(
|
|
231
|
+
ExtractedAuthScheme(
|
|
232
|
+
scheme_type=scheme_type,
|
|
233
|
+
name=name,
|
|
234
|
+
location=CodeLocation(file=parsed_file.path, line=line),
|
|
235
|
+
confidence=Confidence.HIGH,
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# ── Import-based: @ApiBearerAuth / AuthGuard ─────────────────────────
|
|
240
|
+
for imp in parsed_file.imports:
|
|
241
|
+
for name in imp.names:
|
|
242
|
+
if name == "ApiBearerAuth":
|
|
243
|
+
_add("BearerAuth", AuthSchemeType.JWT_BEARER, 1)
|
|
244
|
+
elif name == "AuthGuard":
|
|
245
|
+
_add("AuthGuard", AuthSchemeType.JWT_BEARER, 1)
|
|
246
|
+
elif name in ("JwtAuthGuard", "JwtGuard"):
|
|
247
|
+
_add(name, AuthSchemeType.JWT_BEARER, 1)
|
|
248
|
+
|
|
249
|
+
# ── Class-based: Passport strategy definitions ───────────────────────
|
|
250
|
+
for cls in parsed_file.classes:
|
|
251
|
+
base_names_lower = {b.lower() for b in cls.base_classes}
|
|
252
|
+
if "passportstrategy" in base_names_lower or any(
|
|
253
|
+
"strategy" in b for b in base_names_lower
|
|
254
|
+
):
|
|
255
|
+
hint = cls.name.lower()
|
|
256
|
+
scheme_type = AuthSchemeType.JWT_BEARER if "jwt" in hint else AuthSchemeType.CUSTOM
|
|
257
|
+
line = cls.location.line if cls.location else 1
|
|
258
|
+
_add(cls.name, scheme_type, line)
|
|
259
|
+
|
|
260
|
+
# ── Usage-based: AuthGuard('strategy-name') call sites ───────────────
|
|
261
|
+
for call in parsed_file.call_sites:
|
|
262
|
+
if call.callee_name != "AuthGuard":
|
|
263
|
+
continue
|
|
264
|
+
if call.arguments and call.arguments[0].is_literal:
|
|
265
|
+
strategy = str(call.arguments[0].literal_value)
|
|
266
|
+
scheme_type = (
|
|
267
|
+
AuthSchemeType.JWT_BEARER
|
|
268
|
+
if "jwt" in strategy.lower()
|
|
269
|
+
else AuthSchemeType.CUSTOM
|
|
270
|
+
)
|
|
271
|
+
line = call.location.line if call.location else 1
|
|
272
|
+
_add(f"AuthGuard({strategy})", scheme_type, line)
|
|
273
|
+
|
|
274
|
+
return schemes
|
|
275
|
+
|
|
276
|
+
def extract_auth_dependencies(
|
|
277
|
+
self,
|
|
278
|
+
parsed_file: ParsedFile,
|
|
279
|
+
known_scheme_names: set[str] | None = None,
|
|
280
|
+
**kwargs: Any,
|
|
281
|
+
) -> list[ExtractedAuthDependency]:
|
|
282
|
+
"""
|
|
283
|
+
Detect NestJS auth requirements from decorator patterns.
|
|
284
|
+
|
|
285
|
+
Covers:
|
|
286
|
+
- @UseGuards(AuthGuard('jwt'), RolesGuard) — class or method level
|
|
287
|
+
- @Roles(RoleEnum.admin) / @Roles('admin') — role requirements
|
|
288
|
+
- @ApiBearerAuth() — marks route as requiring Bearer token
|
|
289
|
+
- Class-level decorators apply to all methods in the controller
|
|
290
|
+
|
|
291
|
+
For each controller class with auth decorators, emits one dependency
|
|
292
|
+
capturing the guards and roles. Method-level overrides are also captured.
|
|
293
|
+
"""
|
|
294
|
+
deps: list[ExtractedAuthDependency] = []
|
|
295
|
+
|
|
296
|
+
for cls in parsed_file.classes:
|
|
297
|
+
# Class-level guards and roles (apply to all methods)
|
|
298
|
+
class_guards = _extract_guards(cls.decorators)
|
|
299
|
+
class_roles = _extract_roles(cls.decorators)
|
|
300
|
+
class_bearer = any(d.name == "ApiBearerAuth" for d in cls.decorators)
|
|
301
|
+
|
|
302
|
+
if class_guards or class_roles or class_bearer:
|
|
303
|
+
uses_schemes = _guards_to_scheme_names(class_guards)
|
|
304
|
+
if class_bearer and "BearerAuth" not in uses_schemes:
|
|
305
|
+
uses_schemes.append("BearerAuth")
|
|
306
|
+
line = cls.location.line if cls.location else 1
|
|
307
|
+
deps.append(
|
|
308
|
+
ExtractedAuthDependency(
|
|
309
|
+
name=cls.name,
|
|
310
|
+
qualified_name=QualifiedName(
|
|
311
|
+
module=parsed_file.path.stem,
|
|
312
|
+
name=cls.name,
|
|
313
|
+
),
|
|
314
|
+
location=CodeLocation(file=parsed_file.path, line=line),
|
|
315
|
+
dependency_type=AuthDependencyType.DECORATOR,
|
|
316
|
+
uses_schemes=uses_schemes,
|
|
317
|
+
requires_roles=class_roles,
|
|
318
|
+
confidence=Confidence.HIGH,
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Method-level guards/roles that differ from class-level
|
|
323
|
+
for method in cls.methods:
|
|
324
|
+
method_guards = _extract_guards(method.decorators)
|
|
325
|
+
method_roles = _extract_roles(method.decorators)
|
|
326
|
+
method_bearer = any(d.name == "ApiBearerAuth" for d in method.decorators)
|
|
327
|
+
|
|
328
|
+
if not method_guards and not method_roles and not method_bearer:
|
|
329
|
+
continue
|
|
330
|
+
# Only emit if method has guards/roles not already covered by class
|
|
331
|
+
if method_guards == class_guards and method_roles == class_roles:
|
|
332
|
+
continue
|
|
333
|
+
|
|
334
|
+
uses_schemes = _guards_to_scheme_names(method_guards)
|
|
335
|
+
if method_bearer and "BearerAuth" not in uses_schemes:
|
|
336
|
+
uses_schemes.append("BearerAuth")
|
|
337
|
+
line = method.location.line if method.location else 1
|
|
338
|
+
handler_name = f"{cls.name}.{method.name}"
|
|
339
|
+
deps.append(
|
|
340
|
+
ExtractedAuthDependency(
|
|
341
|
+
name=handler_name,
|
|
342
|
+
qualified_name=QualifiedName(
|
|
343
|
+
module=parsed_file.path.stem,
|
|
344
|
+
name=handler_name,
|
|
345
|
+
),
|
|
346
|
+
location=CodeLocation(file=parsed_file.path, line=line),
|
|
347
|
+
dependency_type=AuthDependencyType.DECORATOR,
|
|
348
|
+
uses_schemes=uses_schemes,
|
|
349
|
+
requires_roles=method_roles,
|
|
350
|
+
confidence=Confidence.HIGH,
|
|
351
|
+
)
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
return deps
|
|
355
|
+
|
|
356
|
+
def extract_middleware(self, parsed_file: ParsedFile) -> list[ExtractedMiddleware]:
|
|
357
|
+
return []
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _find_decorator(
|
|
361
|
+
decorators: list[ParsedDecorator],
|
|
362
|
+
name: str,
|
|
363
|
+
) -> ParsedDecorator | None:
|
|
364
|
+
"""Find a decorator by name in a list."""
|
|
365
|
+
for dec in decorators:
|
|
366
|
+
if dec.name == name:
|
|
367
|
+
return dec
|
|
368
|
+
return None
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _find_verb_decorator(
|
|
372
|
+
decorators: list[ParsedDecorator],
|
|
373
|
+
) -> ParsedDecorator | None:
|
|
374
|
+
"""Find the first HTTP verb decorator in a list."""
|
|
375
|
+
for dec in decorators:
|
|
376
|
+
if dec.name in _VERB_DECORATORS:
|
|
377
|
+
return dec
|
|
378
|
+
return None
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _extract_guards(decorators: list[ParsedDecorator]) -> list[str]:
|
|
382
|
+
"""Extract guard names from @UseGuards(...) decorators."""
|
|
383
|
+
guards: list[str] = []
|
|
384
|
+
for dec in decorators:
|
|
385
|
+
if dec.name != "UseGuards":
|
|
386
|
+
continue
|
|
387
|
+
for arg in dec.positional_args:
|
|
388
|
+
arg_str = str(arg).strip()
|
|
389
|
+
if arg_str:
|
|
390
|
+
guards.append(arg_str)
|
|
391
|
+
return guards
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _extract_roles(decorators: list[ParsedDecorator]) -> list[str]:
|
|
395
|
+
"""Extract role names from @Roles(...) decorators."""
|
|
396
|
+
roles: list[str] = []
|
|
397
|
+
for dec in decorators:
|
|
398
|
+
if dec.name not in ("Roles", "Role"):
|
|
399
|
+
continue
|
|
400
|
+
for arg in dec.positional_args:
|
|
401
|
+
arg_str = str(arg).strip().strip("'\"")
|
|
402
|
+
if arg_str:
|
|
403
|
+
# Strip enum prefix: RoleEnum.admin → admin
|
|
404
|
+
if "." in arg_str:
|
|
405
|
+
arg_str = arg_str.split(".")[-1]
|
|
406
|
+
roles.append(arg_str)
|
|
407
|
+
return roles
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _guards_to_scheme_names(guards: list[str]) -> list[str]:
|
|
411
|
+
"""Map guard names to auth scheme names."""
|
|
412
|
+
schemes: list[str] = []
|
|
413
|
+
for guard in guards:
|
|
414
|
+
low = guard.lower()
|
|
415
|
+
if "jwt" in low or "bearer" in low or "auth" in low:
|
|
416
|
+
if "BearerAuth" not in schemes:
|
|
417
|
+
schemes.append("BearerAuth")
|
|
418
|
+
elif guard not in schemes:
|
|
419
|
+
schemes.append(guard)
|
|
420
|
+
return schemes
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
FrameworkPluginRegistry.register(NestJSPlugin())
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Python framework plugins."""
|
|
2
|
+
|
|
3
|
+
from .celery_plugin import CeleryPlugin
|
|
4
|
+
from .click_plugin import ClickPlugin
|
|
5
|
+
from .django_plugin import DjangoPlugin
|
|
6
|
+
from .fastapi import FastAPIPlugin
|
|
7
|
+
from .graphql_plugin import GraphQLPythonPlugin
|
|
8
|
+
from .prefect_plugin import PrefectPlugin
|
|
9
|
+
from .webhook_plugin import WebhookEventPlugin
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"FastAPIPlugin",
|
|
13
|
+
"ClickPlugin",
|
|
14
|
+
"CeleryPlugin",
|
|
15
|
+
"DjangoPlugin",
|
|
16
|
+
"GraphQLPythonPlugin",
|
|
17
|
+
"PrefectPlugin",
|
|
18
|
+
"WebhookEventPlugin",
|
|
19
|
+
]
|