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,8 @@
|
|
|
1
|
+
"""JavaScript/TypeScript framework plugins."""
|
|
2
|
+
|
|
3
|
+
from .express_plugin import ExpressPlugin
|
|
4
|
+
from .fastify_plugin import FastifyPlugin # noqa: F401 — triggers registration
|
|
5
|
+
from .graphql_plugin import JavaScriptGraphQLPlugin
|
|
6
|
+
from .nestjs_plugin import NestJSPlugin
|
|
7
|
+
|
|
8
|
+
__all__ = ["ExpressPlugin", "FastifyPlugin", "JavaScriptGraphQLPlugin", "NestJSPlugin"]
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Express.js framework plugin.
|
|
3
|
+
|
|
4
|
+
Extracts HTTP routes registered via express.Router() and app.get/post/etc.
|
|
5
|
+
Handles:
|
|
6
|
+
- Direct method calls: app.get('/path', handler)
|
|
7
|
+
- Router instance: router.post('/path', handler)
|
|
8
|
+
- Path parameters: /users/:id → /users/{id}
|
|
9
|
+
- Cross-file prefix from url_prefix_map (populated by url_prefix_resolver)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import re
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
from ...core.types import (
|
|
19
|
+
AuthDependencyType,
|
|
20
|
+
AuthSchemeType,
|
|
21
|
+
CodeLocation,
|
|
22
|
+
Confidence,
|
|
23
|
+
Framework,
|
|
24
|
+
HttpMethod,
|
|
25
|
+
Language,
|
|
26
|
+
ParameterLocation,
|
|
27
|
+
QualifiedName,
|
|
28
|
+
)
|
|
29
|
+
from ...parsing.base import ParsedFile
|
|
30
|
+
from ..base import (
|
|
31
|
+
BaseFrameworkPlugin,
|
|
32
|
+
ExtractedAuthDependency,
|
|
33
|
+
ExtractedAuthScheme,
|
|
34
|
+
ExtractedDependency,
|
|
35
|
+
ExtractedMiddleware,
|
|
36
|
+
ExtractedParameter,
|
|
37
|
+
ExtractedRoute,
|
|
38
|
+
FrameworkPluginRegistry,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if TYPE_CHECKING:
|
|
42
|
+
from ...parsing.services import AnalysisContext
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger(__name__)
|
|
45
|
+
|
|
46
|
+
# HTTP methods handled by Express router
|
|
47
|
+
_EXPRESS_HTTP_METHODS: frozenset[str] = frozenset(
|
|
48
|
+
{
|
|
49
|
+
"get",
|
|
50
|
+
"post",
|
|
51
|
+
"put",
|
|
52
|
+
"patch",
|
|
53
|
+
"delete",
|
|
54
|
+
"options",
|
|
55
|
+
"head",
|
|
56
|
+
"all",
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Regex to convert Express :param to {param}
|
|
61
|
+
_PARAM_RE = re.compile(r":([A-Za-z_]\w*)")
|
|
62
|
+
|
|
63
|
+
# ── Auth detection constants ──────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
# Module names (or fragments) that indicate JWT auth
|
|
66
|
+
_JWT_MODULE_HINTS = (
|
|
67
|
+
"jsonwebtoken",
|
|
68
|
+
"express-jwt",
|
|
69
|
+
"passport-jwt",
|
|
70
|
+
"jwt-simple",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Variable/import name substrings that indicate an auth middleware
|
|
74
|
+
_AUTH_MIDDLEWARE_HINTS = (
|
|
75
|
+
"auth",
|
|
76
|
+
"authentication",
|
|
77
|
+
"authorize",
|
|
78
|
+
"authorization",
|
|
79
|
+
"jwt",
|
|
80
|
+
"passport",
|
|
81
|
+
"token",
|
|
82
|
+
"apikey",
|
|
83
|
+
"api_key",
|
|
84
|
+
"bearer",
|
|
85
|
+
"security",
|
|
86
|
+
"verify",
|
|
87
|
+
"guard",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Variable name substrings that indicate API key middleware
|
|
91
|
+
_API_KEY_HINTS = ("apikey", "api_key", "apikeyrepo", "header.api_key")
|
|
92
|
+
|
|
93
|
+
# Call names that indicate JWT operations
|
|
94
|
+
_JWT_CALL_HINTS = ("jwt.verify", "jwt.sign", "jwt.decode", "jwt.validate", "JWT.validate")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _colon_to_curly(path: str) -> str:
|
|
98
|
+
"""Convert Express-style :param to OpenAPI-style {param}."""
|
|
99
|
+
return _PARAM_RE.sub(r"{\1}", path)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _extract_path_params(path: str) -> list[ExtractedParameter]:
|
|
103
|
+
"""Extract path parameters from a route path string."""
|
|
104
|
+
return [
|
|
105
|
+
ExtractedParameter(name=m.group(1), location=ParameterLocation.PATH)
|
|
106
|
+
for m in _PARAM_RE.finditer(path)
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ExpressPlugin(BaseFrameworkPlugin):
|
|
111
|
+
"""
|
|
112
|
+
Framework plugin for Express.js.
|
|
113
|
+
|
|
114
|
+
Detects Express usage and extracts route definitions from
|
|
115
|
+
router.get/post/put/delete/patch/options/head/all() calls.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
FRAMEWORK = Framework.EXPRESS
|
|
119
|
+
LANGUAGE = Language.JAVASCRIPT
|
|
120
|
+
DETECTION_IMPORTS: frozenset[str] = frozenset({"express"})
|
|
121
|
+
|
|
122
|
+
def detect(self, parsed_file: ParsedFile) -> bool:
|
|
123
|
+
"""Detect Express by looking for 'express' in imports."""
|
|
124
|
+
for imp in parsed_file.imports:
|
|
125
|
+
if imp.module == "express" or imp.module.startswith("express/"):
|
|
126
|
+
return True
|
|
127
|
+
# require('express') parsed as an import with module='express'
|
|
128
|
+
for name in imp.names:
|
|
129
|
+
if name == "express":
|
|
130
|
+
return True
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
def extract_routes(
|
|
134
|
+
self,
|
|
135
|
+
parsed_file: ParsedFile,
|
|
136
|
+
context: AnalysisContext | None = None,
|
|
137
|
+
) -> list[ExtractedRoute]:
|
|
138
|
+
"""Extract Express route registrations from call sites."""
|
|
139
|
+
routes: list[ExtractedRoute] = []
|
|
140
|
+
|
|
141
|
+
# Get per-file prefix from cross-file prefix map (if available)
|
|
142
|
+
file_prefix = ""
|
|
143
|
+
if context and context.language_services:
|
|
144
|
+
prefix_map = context.language_services.get("_url_prefix_map")
|
|
145
|
+
if prefix_map:
|
|
146
|
+
# Use resolved path so symlinks (e.g. /var→/private/var on macOS)
|
|
147
|
+
# don't cause mismatches with the resolver's map keys.
|
|
148
|
+
file_key = str(parsed_file.path.resolve())
|
|
149
|
+
prefixes = prefix_map.get(file_key, [])
|
|
150
|
+
if prefixes:
|
|
151
|
+
file_prefix = prefixes[0] # Use the first prefix
|
|
152
|
+
|
|
153
|
+
for call in parsed_file.call_sites:
|
|
154
|
+
if not call.is_method_call:
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
method_lower = call.callee_name.lower()
|
|
158
|
+
if method_lower not in _EXPRESS_HTTP_METHODS:
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
# First argument must be a string literal (the path)
|
|
162
|
+
if not call.arguments:
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
path_arg = call.arguments[0]
|
|
166
|
+
if not path_arg.is_literal or not isinstance(path_arg.literal_value, str):
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
raw_path = path_arg.literal_value
|
|
170
|
+
path = _colon_to_curly(raw_path)
|
|
171
|
+
|
|
172
|
+
# Apply file-level prefix
|
|
173
|
+
if file_prefix:
|
|
174
|
+
path = file_prefix.rstrip("/") + "/" + path.lstrip("/")
|
|
175
|
+
|
|
176
|
+
# Normalize double slashes and strip trailing slash (except root)
|
|
177
|
+
path = re.sub(r"/+", "/", path)
|
|
178
|
+
if not path.startswith("/"):
|
|
179
|
+
path = "/" + path
|
|
180
|
+
if path != "/" and path.endswith("/"):
|
|
181
|
+
path = path.rstrip("/")
|
|
182
|
+
|
|
183
|
+
# Get handler name from second argument (variable or arrow function)
|
|
184
|
+
handler_name = "<anonymous>"
|
|
185
|
+
if len(call.arguments) > 1:
|
|
186
|
+
handler_arg = call.arguments[1]
|
|
187
|
+
if handler_arg.is_variable and handler_arg.variable_name:
|
|
188
|
+
handler_name = handler_arg.variable_name
|
|
189
|
+
elif handler_arg.is_expression and handler_arg.expression_text:
|
|
190
|
+
handler_name = "<arrow>"
|
|
191
|
+
|
|
192
|
+
# HTTP method
|
|
193
|
+
http_method = _method_from_str(method_lower)
|
|
194
|
+
|
|
195
|
+
# Path params (from original raw path)
|
|
196
|
+
path_params = _extract_path_params(raw_path)
|
|
197
|
+
|
|
198
|
+
routes.append(
|
|
199
|
+
ExtractedRoute(
|
|
200
|
+
method=http_method,
|
|
201
|
+
path=path,
|
|
202
|
+
handler_function=QualifiedName(
|
|
203
|
+
module=parsed_file.path.stem,
|
|
204
|
+
name=handler_name,
|
|
205
|
+
),
|
|
206
|
+
handler_location=call.location,
|
|
207
|
+
path_params=path_params,
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return routes
|
|
212
|
+
|
|
213
|
+
def extract_dependencies(self, parsed_file: ParsedFile) -> list[ExtractedDependency]:
|
|
214
|
+
return []
|
|
215
|
+
|
|
216
|
+
def extract_auth_schemes(self, parsed_file: ParsedFile) -> list[ExtractedAuthScheme]:
|
|
217
|
+
"""
|
|
218
|
+
Detect Express auth scheme definitions.
|
|
219
|
+
|
|
220
|
+
Covers:
|
|
221
|
+
- JWT library imports (jsonwebtoken, express-jwt, passport-jwt) → JWT_BEARER
|
|
222
|
+
- Local JWT class/util imports (files named 'jwt', 'JWT', 'authentication')
|
|
223
|
+
- API key patterns: imports of local files named 'apikey', 'api-key'
|
|
224
|
+
- JWT call sites (jwt.verify, JWT.validate) in the file
|
|
225
|
+
"""
|
|
226
|
+
schemes: list[ExtractedAuthScheme] = []
|
|
227
|
+
seen: set[str] = set()
|
|
228
|
+
file_loc = CodeLocation(file=parsed_file.path, line=1)
|
|
229
|
+
|
|
230
|
+
def _add(name: str, scheme_type: AuthSchemeType) -> None:
|
|
231
|
+
if name not in seen:
|
|
232
|
+
seen.add(name)
|
|
233
|
+
schemes.append(
|
|
234
|
+
ExtractedAuthScheme(
|
|
235
|
+
scheme_type=scheme_type,
|
|
236
|
+
name=name,
|
|
237
|
+
location=file_loc,
|
|
238
|
+
confidence=Confidence.HIGH,
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# ── Import-based detection ───────────────────────────────────────────
|
|
243
|
+
for imp in parsed_file.imports:
|
|
244
|
+
mod = imp.module.lower()
|
|
245
|
+
# Third-party JWT packages
|
|
246
|
+
if any(hint in mod for hint in _JWT_MODULE_HINTS):
|
|
247
|
+
_add("JwtBearer", AuthSchemeType.JWT_BEARER)
|
|
248
|
+
# Local module paths that look like JWT/auth utilities
|
|
249
|
+
elif imp.module.startswith((".", "/")):
|
|
250
|
+
stem = mod.rsplit("/", 1)[-1]
|
|
251
|
+
if any(hint in stem for hint in ("jwt", "authentication", "auth")):
|
|
252
|
+
_add("JwtBearer", AuthSchemeType.JWT_BEARER)
|
|
253
|
+
elif any(hint in stem for hint in ("apikey", "api-key", "api_key")):
|
|
254
|
+
_add("ApiKey", AuthSchemeType.API_KEY_HEADER)
|
|
255
|
+
# passport
|
|
256
|
+
elif mod.startswith("passport"):
|
|
257
|
+
_add("Passport", AuthSchemeType.JWT_BEARER)
|
|
258
|
+
|
|
259
|
+
# ── Call-site-based detection ────────────────────────────────────────
|
|
260
|
+
for call in parsed_file.call_sites:
|
|
261
|
+
callee = call.callee_name.lower()
|
|
262
|
+
if callee in ("verify", "validate", "decode") and call.is_method_call:
|
|
263
|
+
_add("JwtBearer", AuthSchemeType.JWT_BEARER)
|
|
264
|
+
elif callee == "authenticate":
|
|
265
|
+
_add("Passport", AuthSchemeType.JWT_BEARER)
|
|
266
|
+
|
|
267
|
+
return schemes
|
|
268
|
+
|
|
269
|
+
def extract_auth_dependencies(
|
|
270
|
+
self,
|
|
271
|
+
parsed_file: ParsedFile,
|
|
272
|
+
known_scheme_names: set[str] | None = None,
|
|
273
|
+
**kwargs: Any,
|
|
274
|
+
) -> list[ExtractedAuthDependency]:
|
|
275
|
+
"""
|
|
276
|
+
Detect Express auth middleware applied via router.use().
|
|
277
|
+
|
|
278
|
+
Covers:
|
|
279
|
+
- router.use(authentication, ...) — JWT auth middleware by variable name
|
|
280
|
+
- router.use(apikey) — API key middleware by variable name
|
|
281
|
+
- router.use(authentication, role(RoleCode.WRITER), authorization) — role chain
|
|
282
|
+
- router.use(role(RoleCode.X)) — extracts role name from role() call
|
|
283
|
+
|
|
284
|
+
Auth middleware is identified by name heuristic: variables whose names
|
|
285
|
+
contain 'auth', 'authentication', 'authorization', 'apikey', 'jwt', etc.
|
|
286
|
+
Role names are extracted from role(RoleCode.X) call arguments.
|
|
287
|
+
"""
|
|
288
|
+
deps: list[ExtractedAuthDependency] = []
|
|
289
|
+
|
|
290
|
+
# Build set of locally imported names that hint at auth
|
|
291
|
+
auth_imports: set[str] = set()
|
|
292
|
+
for imp in parsed_file.imports:
|
|
293
|
+
mod_lower = imp.module.lower()
|
|
294
|
+
is_auth_module = any(h in mod_lower for h in _AUTH_MIDDLEWARE_HINTS)
|
|
295
|
+
for name in imp.names:
|
|
296
|
+
if is_auth_module or any(h in name.lower() for h in _AUTH_MIDDLEWARE_HINTS):
|
|
297
|
+
auth_imports.add(name)
|
|
298
|
+
|
|
299
|
+
for call in parsed_file.call_sites:
|
|
300
|
+
if not call.is_method_call:
|
|
301
|
+
continue
|
|
302
|
+
if call.callee_name.lower() != "use":
|
|
303
|
+
continue
|
|
304
|
+
if not call.arguments:
|
|
305
|
+
continue
|
|
306
|
+
|
|
307
|
+
# Collect auth middleware names and roles from args
|
|
308
|
+
middleware_names: list[str] = []
|
|
309
|
+
requires_roles: list[str] = []
|
|
310
|
+
|
|
311
|
+
for arg in call.arguments:
|
|
312
|
+
if arg.is_variable and arg.variable_name:
|
|
313
|
+
vname = arg.variable_name
|
|
314
|
+
if (
|
|
315
|
+
any(h in vname.lower() for h in _AUTH_MIDDLEWARE_HINTS)
|
|
316
|
+
or vname in auth_imports
|
|
317
|
+
):
|
|
318
|
+
middleware_names.append(vname)
|
|
319
|
+
elif arg.is_expression and arg.expression_text:
|
|
320
|
+
expr = arg.expression_text
|
|
321
|
+
# Detect role(RoleCode.X) or role('X') call
|
|
322
|
+
role_match = re.match(r"role\s*\(\s*(?:RoleCode\.)?([A-Za-z_]\w*)\s*\)", expr)
|
|
323
|
+
if role_match:
|
|
324
|
+
requires_roles.append(role_match.group(1))
|
|
325
|
+
# Detect passport.authenticate('strategy')
|
|
326
|
+
elif "authenticate" in expr.lower():
|
|
327
|
+
strat_match = re.search(r"authenticate\s*\(\s*['\"]([^'\"]+)['\"]", expr)
|
|
328
|
+
if strat_match:
|
|
329
|
+
middleware_names.append(f"passport:{strat_match.group(1)}")
|
|
330
|
+
else:
|
|
331
|
+
middleware_names.append("passport")
|
|
332
|
+
# Other expression-based middleware with auth hint
|
|
333
|
+
elif any(h in expr.lower() for h in _AUTH_MIDDLEWARE_HINTS):
|
|
334
|
+
name_hint = expr.split("(")[0].strip()
|
|
335
|
+
middleware_names.append(name_hint)
|
|
336
|
+
|
|
337
|
+
if not middleware_names and not requires_roles:
|
|
338
|
+
continue
|
|
339
|
+
|
|
340
|
+
uses_schemes: list[str] = []
|
|
341
|
+
for name in middleware_names:
|
|
342
|
+
low = name.lower()
|
|
343
|
+
if any(h in low for h in ("jwt", "authentication", "bearer", "token")):
|
|
344
|
+
if "JwtBearer" not in uses_schemes:
|
|
345
|
+
uses_schemes.append("JwtBearer")
|
|
346
|
+
elif any(h in low for h in _API_KEY_HINTS):
|
|
347
|
+
if "ApiKey" not in uses_schemes:
|
|
348
|
+
uses_schemes.append("ApiKey")
|
|
349
|
+
elif "passport" in low:
|
|
350
|
+
if "Passport" not in uses_schemes:
|
|
351
|
+
uses_schemes.append("Passport")
|
|
352
|
+
|
|
353
|
+
loc = call.location or CodeLocation(file=parsed_file.path, line=1)
|
|
354
|
+
dep_name = "+".join(middleware_names) if middleware_names else "role"
|
|
355
|
+
deps.append(
|
|
356
|
+
ExtractedAuthDependency(
|
|
357
|
+
name=dep_name,
|
|
358
|
+
qualified_name=QualifiedName(
|
|
359
|
+
module=parsed_file.path.stem,
|
|
360
|
+
name=dep_name,
|
|
361
|
+
),
|
|
362
|
+
location=loc,
|
|
363
|
+
dependency_type=AuthDependencyType.MIDDLEWARE,
|
|
364
|
+
uses_schemes=uses_schemes,
|
|
365
|
+
requires_roles=requires_roles,
|
|
366
|
+
confidence=Confidence.HIGH,
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
return deps
|
|
371
|
+
|
|
372
|
+
def extract_middleware(self, parsed_file: ParsedFile) -> list[ExtractedMiddleware]:
|
|
373
|
+
return []
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _method_from_str(method: str) -> HttpMethod:
|
|
377
|
+
"""Convert a lowercase HTTP method string to HttpMethod enum."""
|
|
378
|
+
_MAP = {
|
|
379
|
+
"get": HttpMethod.GET,
|
|
380
|
+
"post": HttpMethod.POST,
|
|
381
|
+
"put": HttpMethod.PUT,
|
|
382
|
+
"patch": HttpMethod.PATCH,
|
|
383
|
+
"delete": HttpMethod.DELETE,
|
|
384
|
+
"options": HttpMethod.OPTIONS,
|
|
385
|
+
"head": HttpMethod.HEAD,
|
|
386
|
+
"all": HttpMethod.GET, # 'all' → GET as a fallback
|
|
387
|
+
}
|
|
388
|
+
return _MAP.get(method, HttpMethod.GET)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
FrameworkPluginRegistry.register(ExpressPlugin())
|