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,437 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared auth extraction helpers used across multiple framework plugins.
|
|
3
|
+
|
|
4
|
+
GraphQL resolvers use the same auth annotations/decorators as their parent REST
|
|
5
|
+
frameworks — Spring @PreAuthorize, NestJS @UseGuards/@Roles, Python permission_classes.
|
|
6
|
+
These helpers centralise the logic so it can be called from both the REST plugins
|
|
7
|
+
and the corresponding GraphQL plugins without duplication.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from ..core.types import (
|
|
16
|
+
AuthDependencyType,
|
|
17
|
+
AuthSchemeType,
|
|
18
|
+
CodeLocation,
|
|
19
|
+
Confidence,
|
|
20
|
+
QualifiedName,
|
|
21
|
+
)
|
|
22
|
+
from .base import ExtractedAuthDependency, ExtractedAuthScheme
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from ..parsing.base import ParsedDecorator, ParsedFile
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
# Java / Spring Security helpers
|
|
30
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
_JAVA_AUTH_ANNOTATIONS = frozenset({"PreAuthorize", "Secured", "RolesAllowed"})
|
|
33
|
+
|
|
34
|
+
_SPRING_SECURITY_IMPORTS = frozenset(
|
|
35
|
+
{
|
|
36
|
+
"org.springframework.security.access.prepost.PreAuthorize",
|
|
37
|
+
"org.springframework.security.access.annotation.Secured",
|
|
38
|
+
"javax.annotation.security.RolesAllowed",
|
|
39
|
+
"jakarta.annotation.security.RolesAllowed",
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def extract_java_auth_schemes(parsed_file: ParsedFile) -> list[ExtractedAuthScheme]:
|
|
45
|
+
"""
|
|
46
|
+
Detect Spring Security scheme from imports of @PreAuthorize/@Secured.
|
|
47
|
+
|
|
48
|
+
Emits a SPRING_SECURITY scheme when the file imports Spring Security
|
|
49
|
+
method-security annotations, signalling that access control is active.
|
|
50
|
+
"""
|
|
51
|
+
schemes: list[ExtractedAuthScheme] = []
|
|
52
|
+
for imp in parsed_file.imports:
|
|
53
|
+
if imp.module in _SPRING_SECURITY_IMPORTS or any(
|
|
54
|
+
name in _JAVA_AUTH_ANNOTATIONS for name in imp.names
|
|
55
|
+
):
|
|
56
|
+
schemes.append(
|
|
57
|
+
ExtractedAuthScheme(
|
|
58
|
+
scheme_type=AuthSchemeType.SPRING_SECURITY,
|
|
59
|
+
name="SpringSecurity",
|
|
60
|
+
location=CodeLocation(
|
|
61
|
+
file=parsed_file.path, line=imp.location.line if imp.location else 1
|
|
62
|
+
),
|
|
63
|
+
confidence=Confidence.HIGH,
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
break # one scheme per file is enough
|
|
67
|
+
return schemes
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def extract_java_auth_dependencies(parsed_file: ParsedFile) -> list[ExtractedAuthDependency]:
|
|
71
|
+
"""
|
|
72
|
+
Extract @PreAuthorize, @Secured, @RolesAllowed on Java methods and classes.
|
|
73
|
+
|
|
74
|
+
Populates requires_roles from SpEL hasRole/hasAuthority expressions and
|
|
75
|
+
from @Secured / @RolesAllowed string arrays.
|
|
76
|
+
"""
|
|
77
|
+
deps: list[ExtractedAuthDependency] = []
|
|
78
|
+
|
|
79
|
+
for cls in parsed_file.classes:
|
|
80
|
+
class_roles: list[str] = []
|
|
81
|
+
for dec in cls.decorators:
|
|
82
|
+
if dec.name in _JAVA_AUTH_ANNOTATIONS:
|
|
83
|
+
class_roles.extend(_extract_java_roles(dec))
|
|
84
|
+
|
|
85
|
+
for method in cls.methods:
|
|
86
|
+
method_roles: list[str] = list(class_roles)
|
|
87
|
+
for dec in method.decorators:
|
|
88
|
+
if dec.name in _JAVA_AUTH_ANNOTATIONS:
|
|
89
|
+
method_roles.extend(_extract_java_roles(dec))
|
|
90
|
+
|
|
91
|
+
if not method_roles:
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
loc = method.location or CodeLocation(file=parsed_file.path, line=1)
|
|
95
|
+
handler_name = f"{cls.name}.{method.name}"
|
|
96
|
+
deps.append(
|
|
97
|
+
ExtractedAuthDependency(
|
|
98
|
+
name=handler_name,
|
|
99
|
+
qualified_name=QualifiedName(module=parsed_file.path.stem, name=handler_name),
|
|
100
|
+
location=loc,
|
|
101
|
+
dependency_type=AuthDependencyType.ANNOTATION,
|
|
102
|
+
uses_schemes=["SpringSecurity"],
|
|
103
|
+
requires_roles=method_roles,
|
|
104
|
+
confidence=Confidence.HIGH,
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return deps
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _extract_java_roles(dec: ParsedDecorator) -> list[str]:
|
|
112
|
+
"""Extract role names from @PreAuthorize/@Secured/@RolesAllowed decorator."""
|
|
113
|
+
roles: list[str] = []
|
|
114
|
+
for arg in dec.positional_args:
|
|
115
|
+
arg_str = str(arg)
|
|
116
|
+
# SpEL: hasRole('ADMIN'), hasAuthority('SCOPE_MANAGER'), isAuthenticated()
|
|
117
|
+
for m in re.finditer(r"has(?:Role|Authority)\s*\(\s*['\"]([^'\"]+)['\"]", arg_str):
|
|
118
|
+
roles.append(m.group(1))
|
|
119
|
+
# @Secured / @RolesAllowed array: {"ROLE_ADMIN", "ROLE_USER"}
|
|
120
|
+
for m in re.finditer(r"['\"]([A-Z_][A-Z0-9_]*)['\"]", arg_str):
|
|
121
|
+
role = m.group(1)
|
|
122
|
+
if role not in roles:
|
|
123
|
+
roles.append(role)
|
|
124
|
+
return roles
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
128
|
+
# NestJS / TypeGraphQL helpers (reused by JS GraphQL plugin)
|
|
129
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
_NESTJS_BEARER_IMPORTS = frozenset({"ApiBearerAuth", "AuthGuard", "JwtAuthGuard", "JwtGuard"})
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def extract_nestjs_auth_schemes(parsed_file: ParsedFile) -> list[ExtractedAuthScheme]:
|
|
135
|
+
"""Detect JWT Bearer scheme from NestJS/TypeGraphQL auth imports."""
|
|
136
|
+
schemes: list[ExtractedAuthScheme] = []
|
|
137
|
+
seen: set[str] = set()
|
|
138
|
+
|
|
139
|
+
for imp in parsed_file.imports:
|
|
140
|
+
for name in imp.names:
|
|
141
|
+
if name in _NESTJS_BEARER_IMPORTS and "BearerAuth" not in seen:
|
|
142
|
+
seen.add("BearerAuth")
|
|
143
|
+
schemes.append(
|
|
144
|
+
ExtractedAuthScheme(
|
|
145
|
+
scheme_type=AuthSchemeType.JWT_BEARER,
|
|
146
|
+
name="BearerAuth",
|
|
147
|
+
location=CodeLocation(file=parsed_file.path, line=1),
|
|
148
|
+
confidence=Confidence.HIGH,
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
elif name == "AuthGuard" and "AuthGuard" not in seen:
|
|
152
|
+
seen.add("AuthGuard")
|
|
153
|
+
# Extract strategy name from AuthGuard('jwt') call sites
|
|
154
|
+
for call in parsed_file.call_sites:
|
|
155
|
+
if call.callee_name == "AuthGuard" and call.arguments:
|
|
156
|
+
arg = call.arguments[0]
|
|
157
|
+
if arg.is_literal:
|
|
158
|
+
strategy = str(arg.literal_value)
|
|
159
|
+
key = f"AuthGuard({strategy})"
|
|
160
|
+
if key not in seen:
|
|
161
|
+
seen.add(key)
|
|
162
|
+
schemes.append(
|
|
163
|
+
ExtractedAuthScheme(
|
|
164
|
+
scheme_type=AuthSchemeType.JWT_BEARER,
|
|
165
|
+
name=key,
|
|
166
|
+
location=CodeLocation(
|
|
167
|
+
file=parsed_file.path,
|
|
168
|
+
line=call.location.line if call.location else 1,
|
|
169
|
+
),
|
|
170
|
+
confidence=Confidence.HIGH,
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return schemes
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def extract_nestjs_auth_dependencies(parsed_file: ParsedFile) -> list[ExtractedAuthDependency]:
|
|
178
|
+
"""
|
|
179
|
+
Extract @UseGuards, @Roles, @ApiBearerAuth from NestJS/TypeGraphQL resolver classes.
|
|
180
|
+
|
|
181
|
+
Class-level decorators apply to all methods; method-level overrides are
|
|
182
|
+
captured separately.
|
|
183
|
+
"""
|
|
184
|
+
deps: list[ExtractedAuthDependency] = []
|
|
185
|
+
|
|
186
|
+
for cls in parsed_file.classes:
|
|
187
|
+
class_guards = _nestjs_guards(cls.decorators)
|
|
188
|
+
class_roles = _nestjs_roles(cls.decorators)
|
|
189
|
+
class_bearer = any(d.name == "ApiBearerAuth" for d in cls.decorators)
|
|
190
|
+
|
|
191
|
+
if class_guards or class_roles or class_bearer:
|
|
192
|
+
uses = _guards_to_schemes(class_guards)
|
|
193
|
+
if class_bearer and "BearerAuth" not in uses:
|
|
194
|
+
uses.append("BearerAuth")
|
|
195
|
+
loc = cls.location or CodeLocation(file=parsed_file.path, line=1)
|
|
196
|
+
deps.append(
|
|
197
|
+
ExtractedAuthDependency(
|
|
198
|
+
name=cls.name,
|
|
199
|
+
qualified_name=QualifiedName(module=parsed_file.path.stem, name=cls.name),
|
|
200
|
+
location=loc,
|
|
201
|
+
dependency_type=AuthDependencyType.DECORATOR,
|
|
202
|
+
uses_schemes=uses,
|
|
203
|
+
requires_roles=class_roles,
|
|
204
|
+
confidence=Confidence.HIGH,
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
for method in cls.methods:
|
|
209
|
+
mg = _nestjs_guards(method.decorators)
|
|
210
|
+
mr = _nestjs_roles(method.decorators)
|
|
211
|
+
mb = any(d.name == "ApiBearerAuth" for d in method.decorators)
|
|
212
|
+
if not mg and not mr and not mb:
|
|
213
|
+
continue
|
|
214
|
+
if mg == class_guards and mr == class_roles:
|
|
215
|
+
continue
|
|
216
|
+
uses = _guards_to_schemes(mg)
|
|
217
|
+
if mb and "BearerAuth" not in uses:
|
|
218
|
+
uses.append("BearerAuth")
|
|
219
|
+
loc = method.location or CodeLocation(file=parsed_file.path, line=1)
|
|
220
|
+
name = f"{cls.name}.{method.name}"
|
|
221
|
+
deps.append(
|
|
222
|
+
ExtractedAuthDependency(
|
|
223
|
+
name=name,
|
|
224
|
+
qualified_name=QualifiedName(module=parsed_file.path.stem, name=name),
|
|
225
|
+
location=loc,
|
|
226
|
+
dependency_type=AuthDependencyType.DECORATOR,
|
|
227
|
+
uses_schemes=uses,
|
|
228
|
+
requires_roles=mr,
|
|
229
|
+
confidence=Confidence.HIGH,
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return deps
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _nestjs_guards(decorators: list[ParsedDecorator]) -> list[str]:
|
|
237
|
+
guards: list[str] = []
|
|
238
|
+
for dec in decorators:
|
|
239
|
+
if dec.name == "UseGuards":
|
|
240
|
+
for arg in dec.positional_args:
|
|
241
|
+
s = str(arg).strip()
|
|
242
|
+
if s:
|
|
243
|
+
guards.append(s)
|
|
244
|
+
return guards
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _nestjs_roles(decorators: list[ParsedDecorator]) -> list[str]:
|
|
248
|
+
roles: list[str] = []
|
|
249
|
+
for dec in decorators:
|
|
250
|
+
if dec.name in ("Roles", "Role"):
|
|
251
|
+
for arg in dec.positional_args:
|
|
252
|
+
s = str(arg).strip().strip("'\"")
|
|
253
|
+
if "." in s:
|
|
254
|
+
s = s.split(".")[-1]
|
|
255
|
+
if s:
|
|
256
|
+
roles.append(s)
|
|
257
|
+
return roles
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _guards_to_schemes(guards: list[str]) -> list[str]:
|
|
261
|
+
schemes: list[str] = []
|
|
262
|
+
for g in guards:
|
|
263
|
+
if any(kw in g.lower() for kw in ("jwt", "bearer", "auth")):
|
|
264
|
+
if "BearerAuth" not in schemes:
|
|
265
|
+
schemes.append("BearerAuth")
|
|
266
|
+
elif g not in schemes:
|
|
267
|
+
schemes.append(g)
|
|
268
|
+
return schemes
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
272
|
+
# Python GraphQL (Strawberry / Graphene) helpers
|
|
273
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
_PYTHON_GQL_AUTH_IMPORTS = frozenset(
|
|
276
|
+
{
|
|
277
|
+
"IsAuthenticated",
|
|
278
|
+
"IsAdminUser",
|
|
279
|
+
"IsAuthenticatedOrReadOnly",
|
|
280
|
+
"AllowAny",
|
|
281
|
+
"BasePermission",
|
|
282
|
+
"login_required",
|
|
283
|
+
"permission_required",
|
|
284
|
+
}
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
_PYTHON_GQL_JWT_MODULES = (
|
|
288
|
+
"rest_framework_jwt",
|
|
289
|
+
"rest_framework_simplejwt",
|
|
290
|
+
"djangorestframework_simplejwt",
|
|
291
|
+
"jose",
|
|
292
|
+
"jwt",
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def extract_python_graphql_auth_schemes(parsed_file: ParsedFile) -> list[ExtractedAuthScheme]:
|
|
297
|
+
"""
|
|
298
|
+
Detect auth schemes in Python GraphQL files (Strawberry/Graphene/Ariadne).
|
|
299
|
+
|
|
300
|
+
Covers:
|
|
301
|
+
- DRF permission class imports → SESSION_COOKIE / JWT_BEARER
|
|
302
|
+
- JWT library imports → JWT_BEARER
|
|
303
|
+
- Strawberry permission_classes decorator argument containing auth classes
|
|
304
|
+
"""
|
|
305
|
+
schemes: list[ExtractedAuthScheme] = []
|
|
306
|
+
seen: set[str] = set()
|
|
307
|
+
|
|
308
|
+
for imp in parsed_file.imports:
|
|
309
|
+
mod = imp.module.lower()
|
|
310
|
+
for name in imp.names:
|
|
311
|
+
if (
|
|
312
|
+
name in _PYTHON_GQL_AUTH_IMPORTS
|
|
313
|
+
or "permission" in name.lower()
|
|
314
|
+
or "auth" in name.lower()
|
|
315
|
+
):
|
|
316
|
+
if "session" in name.lower() and "SESSION_COOKIE" not in seen:
|
|
317
|
+
seen.add("SESSION_COOKIE")
|
|
318
|
+
schemes.append(
|
|
319
|
+
ExtractedAuthScheme(
|
|
320
|
+
scheme_type=AuthSchemeType.SESSION_COOKIE,
|
|
321
|
+
name=name,
|
|
322
|
+
location=CodeLocation(file=parsed_file.path, line=1),
|
|
323
|
+
confidence=Confidence.MEDIUM,
|
|
324
|
+
)
|
|
325
|
+
)
|
|
326
|
+
elif (
|
|
327
|
+
"jwt" in name.lower() or "token" in name.lower()
|
|
328
|
+
) and "JWT_BEARER" not in seen:
|
|
329
|
+
seen.add("JWT_BEARER")
|
|
330
|
+
schemes.append(
|
|
331
|
+
ExtractedAuthScheme(
|
|
332
|
+
scheme_type=AuthSchemeType.JWT_BEARER,
|
|
333
|
+
name=name,
|
|
334
|
+
location=CodeLocation(file=parsed_file.path, line=1),
|
|
335
|
+
confidence=Confidence.HIGH,
|
|
336
|
+
)
|
|
337
|
+
)
|
|
338
|
+
elif "isAuthenticated" in name or "IsAuthenticated" in name:
|
|
339
|
+
if "SESSION_COOKIE" not in seen:
|
|
340
|
+
seen.add("SESSION_COOKIE")
|
|
341
|
+
schemes.append(
|
|
342
|
+
ExtractedAuthScheme(
|
|
343
|
+
scheme_type=AuthSchemeType.SESSION_COOKIE,
|
|
344
|
+
name=name,
|
|
345
|
+
location=CodeLocation(file=parsed_file.path, line=1),
|
|
346
|
+
confidence=Confidence.HIGH,
|
|
347
|
+
)
|
|
348
|
+
)
|
|
349
|
+
# JWT library imports
|
|
350
|
+
if any(kw in mod for kw in _PYTHON_GQL_JWT_MODULES) and "JWT_BEARER" not in seen:
|
|
351
|
+
seen.add("JWT_BEARER")
|
|
352
|
+
schemes.append(
|
|
353
|
+
ExtractedAuthScheme(
|
|
354
|
+
scheme_type=AuthSchemeType.JWT_BEARER,
|
|
355
|
+
name="JwtBearer",
|
|
356
|
+
location=CodeLocation(file=parsed_file.path, line=1),
|
|
357
|
+
confidence=Confidence.HIGH,
|
|
358
|
+
)
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
return schemes
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def extract_python_graphql_auth_dependencies(
|
|
365
|
+
parsed_file: ParsedFile,
|
|
366
|
+
) -> list[ExtractedAuthDependency]:
|
|
367
|
+
"""
|
|
368
|
+
Extract auth requirements from Python GraphQL classes.
|
|
369
|
+
|
|
370
|
+
Covers:
|
|
371
|
+
- Strawberry: @strawberry.type(permission_classes=[IsAuthenticated])
|
|
372
|
+
- @login_required on resolver functions
|
|
373
|
+
- Classes inheriting from IsAuthenticated / BasePermission
|
|
374
|
+
"""
|
|
375
|
+
deps: list[ExtractedAuthDependency] = []
|
|
376
|
+
|
|
377
|
+
# Collect imported auth-related names
|
|
378
|
+
auth_imports: set[str] = set()
|
|
379
|
+
for imp in parsed_file.imports:
|
|
380
|
+
for name in imp.names:
|
|
381
|
+
if name in _PYTHON_GQL_AUTH_IMPORTS or "permission" in name.lower():
|
|
382
|
+
auth_imports.add(name)
|
|
383
|
+
|
|
384
|
+
for cls in parsed_file.classes:
|
|
385
|
+
requires_roles: list[str] = []
|
|
386
|
+
|
|
387
|
+
# Check decorator arguments for permission_classes
|
|
388
|
+
for dec in cls.decorators:
|
|
389
|
+
arg_text = " ".join(str(a) for a in dec.positional_args)
|
|
390
|
+
if "permission_classes" in arg_text or any(n in arg_text for n in auth_imports):
|
|
391
|
+
for n in auth_imports:
|
|
392
|
+
if n in arg_text and n not in requires_roles:
|
|
393
|
+
requires_roles.append(n)
|
|
394
|
+
|
|
395
|
+
# Check class_variables (if parser captures them)
|
|
396
|
+
if "permission_classes" in cls.class_variables:
|
|
397
|
+
for n in auth_imports:
|
|
398
|
+
if n not in requires_roles:
|
|
399
|
+
requires_roles.append(n)
|
|
400
|
+
|
|
401
|
+
if requires_roles:
|
|
402
|
+
loc = cls.location or CodeLocation(file=parsed_file.path, line=1)
|
|
403
|
+
deps.append(
|
|
404
|
+
ExtractedAuthDependency(
|
|
405
|
+
name=cls.name,
|
|
406
|
+
qualified_name=QualifiedName(module=parsed_file.path.stem, name=cls.name),
|
|
407
|
+
location=loc,
|
|
408
|
+
dependency_type=AuthDependencyType.CLASS,
|
|
409
|
+
uses_schemes=[],
|
|
410
|
+
requires_roles=requires_roles,
|
|
411
|
+
confidence=Confidence.MEDIUM,
|
|
412
|
+
)
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# @login_required / @permission_required on functions
|
|
416
|
+
for func in parsed_file.functions:
|
|
417
|
+
roles: list[str] = []
|
|
418
|
+
for dec in func.decorators:
|
|
419
|
+
if dec.name == "login_required":
|
|
420
|
+
roles.append("authenticated")
|
|
421
|
+
elif dec.name == "permission_required" and dec.positional_args:
|
|
422
|
+
roles.append(str(dec.positional_args[0]).strip("'\""))
|
|
423
|
+
if roles:
|
|
424
|
+
loc = func.location or CodeLocation(file=parsed_file.path, line=1)
|
|
425
|
+
deps.append(
|
|
426
|
+
ExtractedAuthDependency(
|
|
427
|
+
name=func.name,
|
|
428
|
+
qualified_name=QualifiedName(module=parsed_file.path.stem, name=func.name),
|
|
429
|
+
location=loc,
|
|
430
|
+
dependency_type=AuthDependencyType.DECORATOR,
|
|
431
|
+
uses_schemes=[],
|
|
432
|
+
requires_roles=roles,
|
|
433
|
+
confidence=Confidence.HIGH,
|
|
434
|
+
)
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
return deps
|