apisec-code-bolt 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. apisec_code_bolt/__init__.py +42 -0
  2. apisec_code_bolt/__main__.py +11 -0
  3. apisec_code_bolt/analysis/__init__.py +96 -0
  4. apisec_code_bolt/analysis/analyzer.py +2309 -0
  5. apisec_code_bolt/analysis/binding_tracker.py +341 -0
  6. apisec_code_bolt/analysis/call_graph.py +1197 -0
  7. apisec_code_bolt/analysis/call_graph_types.py +332 -0
  8. apisec_code_bolt/analysis/call_resolver.py +988 -0
  9. apisec_code_bolt/analysis/capability_tagger.py +322 -0
  10. apisec_code_bolt/analysis/config_scanner.py +197 -0
  11. apisec_code_bolt/analysis/data_flow.py +1883 -0
  12. apisec_code_bolt/analysis/dependency_extractor.py +959 -0
  13. apisec_code_bolt/analysis/flow_analysis.py +1406 -0
  14. apisec_code_bolt/analysis/hof_catalog.py +61 -0
  15. apisec_code_bolt/analysis/integration_detector.py +1399 -0
  16. apisec_code_bolt/analysis/literal_scanner.py +300 -0
  17. apisec_code_bolt/analysis/path_normalizer.py +55 -0
  18. apisec_code_bolt/analysis/read_site_detector.py +310 -0
  19. apisec_code_bolt/analysis/request_patterns.py +162 -0
  20. apisec_code_bolt/analysis/sensitivity_classifier.py +224 -0
  21. apisec_code_bolt/analysis/sink_evidence.py +333 -0
  22. apisec_code_bolt/analysis/url_prefix_resolver.py +338 -0
  23. apisec_code_bolt/cli/__init__.py +5 -0
  24. apisec_code_bolt/cli/exit_codes.py +17 -0
  25. apisec_code_bolt/cli/main.py +1069 -0
  26. apisec_code_bolt/cloud/__init__.py +1 -0
  27. apisec_code_bolt/cloud/apisec_client.py +118 -0
  28. apisec_code_bolt/cloud/client.py +255 -0
  29. apisec_code_bolt/core/__init__.py +75 -0
  30. apisec_code_bolt/core/config.py +528 -0
  31. apisec_code_bolt/core/credentials.py +65 -0
  32. apisec_code_bolt/core/discovery.py +433 -0
  33. apisec_code_bolt/core/log_format.py +115 -0
  34. apisec_code_bolt/core/manifest.py +1009 -0
  35. apisec_code_bolt/core/repo.py +280 -0
  36. apisec_code_bolt/core/state.py +59 -0
  37. apisec_code_bolt/core/telemetry.py +451 -0
  38. apisec_code_bolt/core/types.py +587 -0
  39. apisec_code_bolt/fingerprinting/__init__.py +1 -0
  40. apisec_code_bolt/frameworks/__init__.py +29 -0
  41. apisec_code_bolt/frameworks/_jwt_common.py +50 -0
  42. apisec_code_bolt/frameworks/auth_helpers.py +437 -0
  43. apisec_code_bolt/frameworks/base.py +608 -0
  44. apisec_code_bolt/frameworks/dotnet/__init__.py +17 -0
  45. apisec_code_bolt/frameworks/dotnet/_path_helpers.py +43 -0
  46. apisec_code_bolt/frameworks/dotnet/aspnet_plugin.py +2546 -0
  47. apisec_code_bolt/frameworks/dotnet/grpc_plugin.py +559 -0
  48. apisec_code_bolt/frameworks/dotnet/jwt_config_extractor.py +545 -0
  49. apisec_code_bolt/frameworks/dotnet/legacy_aspnet_plugin.py +732 -0
  50. apisec_code_bolt/frameworks/dotnet/refit_plugin.py +374 -0
  51. apisec_code_bolt/frameworks/dotnet/wcf_plugin.py +1239 -0
  52. apisec_code_bolt/frameworks/java/__init__.py +6 -0
  53. apisec_code_bolt/frameworks/java/_annotations.py +167 -0
  54. apisec_code_bolt/frameworks/java/_constraints.py +128 -0
  55. apisec_code_bolt/frameworks/java/graphql_plugin.py +287 -0
  56. apisec_code_bolt/frameworks/java/jaxrs_plugin.py +748 -0
  57. apisec_code_bolt/frameworks/java/jwt_config_extractor.py +361 -0
  58. apisec_code_bolt/frameworks/java/micronaut_plugin.py +1059 -0
  59. apisec_code_bolt/frameworks/java/spring_plugin.py +1293 -0
  60. apisec_code_bolt/frameworks/js/__init__.py +8 -0
  61. apisec_code_bolt/frameworks/js/express_plugin.py +391 -0
  62. apisec_code_bolt/frameworks/js/fastify_plugin.py +381 -0
  63. apisec_code_bolt/frameworks/js/graphql_plugin.py +198 -0
  64. apisec_code_bolt/frameworks/js/nestjs_plugin.py +423 -0
  65. apisec_code_bolt/frameworks/python/__init__.py +19 -0
  66. apisec_code_bolt/frameworks/python/celery_plugin.py +393 -0
  67. apisec_code_bolt/frameworks/python/click_plugin.py +427 -0
  68. apisec_code_bolt/frameworks/python/django_plugin.py +867 -0
  69. apisec_code_bolt/frameworks/python/fastapi/__init__.py +28 -0
  70. apisec_code_bolt/frameworks/python/fastapi/plugin.py +1390 -0
  71. apisec_code_bolt/frameworks/python/flask_plugin.py +205 -0
  72. apisec_code_bolt/frameworks/python/graphql_plugin.py +274 -0
  73. apisec_code_bolt/frameworks/python/prefect_plugin.py +251 -0
  74. apisec_code_bolt/frameworks/python/webhook_plugin.py +255 -0
  75. apisec_code_bolt/parsing/__init__.py +62 -0
  76. apisec_code_bolt/parsing/base.py +554 -0
  77. apisec_code_bolt/parsing/csharp/__init__.py +5 -0
  78. apisec_code_bolt/parsing/csharp/language_services.py +203 -0
  79. apisec_code_bolt/parsing/csharp/literals.py +72 -0
  80. apisec_code_bolt/parsing/csharp/parser.py +1158 -0
  81. apisec_code_bolt/parsing/csharp/type_resolver.py +568 -0
  82. apisec_code_bolt/parsing/js/__init__.py +5 -0
  83. apisec_code_bolt/parsing/js/language_services.py +118 -0
  84. apisec_code_bolt/parsing/js/parser.py +622 -0
  85. apisec_code_bolt/parsing/jvm/__init__.py +7 -0
  86. apisec_code_bolt/parsing/jvm/language_services.py +270 -0
  87. apisec_code_bolt/parsing/jvm/parser.py +774 -0
  88. apisec_code_bolt/parsing/jvm/type_resolver.py +422 -0
  89. apisec_code_bolt/parsing/python/__init__.py +150 -0
  90. apisec_code_bolt/parsing/python/cbv_extractor.py +606 -0
  91. apisec_code_bolt/parsing/python/constant_resolver.py +500 -0
  92. apisec_code_bolt/parsing/python/cross_file_resolver.py +1054 -0
  93. apisec_code_bolt/parsing/python/dynamic_route_detector.py +532 -0
  94. apisec_code_bolt/parsing/python/expression_utils.py +221 -0
  95. apisec_code_bolt/parsing/python/extraction_types.py +271 -0
  96. apisec_code_bolt/parsing/python/language_services.py +487 -0
  97. apisec_code_bolt/parsing/python/parameter_analyzer.py +789 -0
  98. apisec_code_bolt/parsing/python/parser.py +719 -0
  99. apisec_code_bolt/parsing/python/path_resolver.py +576 -0
  100. apisec_code_bolt/parsing/python/router_registry.py +806 -0
  101. apisec_code_bolt/parsing/python/type_resolver.py +730 -0
  102. apisec_code_bolt/parsing/python/visitors.py +1544 -0
  103. apisec_code_bolt/parsing/services.py +544 -0
  104. apisec_code_bolt/query/__init__.py +1 -0
  105. apisec_code_bolt/query/ast_cache.py +182 -0
  106. apisec_code_bolt/query/executor.py +283 -0
  107. apisec_code_bolt/query/handlers.py +832 -0
  108. apisec_code_bolt-0.1.0.dist-info/METADATA +230 -0
  109. apisec_code_bolt-0.1.0.dist-info/RECORD +111 -0
  110. apisec_code_bolt-0.1.0.dist-info/WHEEL +4 -0
  111. apisec_code_bolt-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,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