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,608 @@
1
+ """
2
+ Base classes and protocols for framework-specific plugins.
3
+
4
+ Each framework (FastAPI, Spring Boot, etc.) implements the FrameworkPlugin
5
+ protocol to provide specialized extraction of routes, dependencies, auth,
6
+ and middleware.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from abc import ABC, abstractmethod
13
+ from dataclasses import dataclass, field
14
+ from typing import TYPE_CHECKING, Any, ClassVar
15
+
16
+ from ..core.types import (
17
+ AuthDependencyType,
18
+ AuthSchemeType,
19
+ CodeLocation,
20
+ Confidence,
21
+ Framework,
22
+ HttpMethod,
23
+ Language,
24
+ ParameterLocation,
25
+ QualifiedName,
26
+ )
27
+
28
+ if TYPE_CHECKING:
29
+ from ..parsing.base import ParsedDecorator, ParsedFile, ParsedFunction
30
+ from ..parsing.services import AnalysisContext
31
+
32
+
33
+ # =============================================================================
34
+ # Route Extraction Types
35
+ # =============================================================================
36
+
37
+
38
+ @dataclass
39
+ class ExtractedParameter:
40
+ """A parameter extracted from a route definition."""
41
+
42
+ name: str
43
+ location: ParameterLocation
44
+
45
+ type_annotation: str | None = None
46
+ required: bool = True
47
+ default_value: str | None = None
48
+
49
+ # Framework-specific details
50
+ alias: str | None = None # Alternative name in request
51
+ description: str | None = None
52
+
53
+ # Validation constraints
54
+ constraints: dict[str, Any] = field(default_factory=dict)
55
+
56
+ # Source location
57
+ code_location: CodeLocation | None = None
58
+
59
+
60
+ @dataclass
61
+ class ExtractedBody:
62
+ """Request body extracted from a route definition."""
63
+
64
+ content_type: str | None = None
65
+ model_name: str | None = None
66
+ model_qualified_name: QualifiedName | None = None
67
+ model_fields: list[str] = field(default_factory=list)
68
+ required: bool = True
69
+
70
+ code_location: CodeLocation | None = None
71
+
72
+
73
+ @dataclass
74
+ class ExtractedResponse:
75
+ """Response model extracted from a route definition."""
76
+
77
+ status_code: int = 200
78
+ model_name: str | None = None
79
+ model_qualified_name: QualifiedName | None = None
80
+ content_type: str | None = None
81
+ description: str | None = None
82
+
83
+
84
+ @dataclass
85
+ class ExtractedRoute:
86
+ """
87
+ An entry point extracted from framework code.
88
+
89
+ This is the framework plugin's output for each detected entry point.
90
+ Covers HTTP routes, CLI commands, task definitions, message consumers, etc.
91
+ """
92
+
93
+ method: HttpMethod
94
+ path: str
95
+
96
+ # Handler
97
+ handler_function: QualifiedName
98
+ handler_location: CodeLocation
99
+
100
+ # Parameters by location
101
+ path_params: list[ExtractedParameter] = field(default_factory=list)
102
+ query_params: list[ExtractedParameter] = field(default_factory=list)
103
+ header_params: list[ExtractedParameter] = field(default_factory=list)
104
+ cookie_params: list[ExtractedParameter] = field(default_factory=list)
105
+
106
+ # Body
107
+ body: ExtractedBody | None = None
108
+
109
+ # Response
110
+ response: ExtractedResponse = field(default_factory=ExtractedResponse)
111
+
112
+ # Framework metadata
113
+ router_name: str | None = None
114
+ tags: list[str] = field(default_factory=list)
115
+ operation_id: str | None = None
116
+ summary: str | None = None
117
+ description: str | None = None
118
+ deprecated: bool = False
119
+
120
+ # Dependencies used by this route
121
+ dependency_refs: list[str] = field(default_factory=list)
122
+
123
+ # Analysis metadata
124
+ confidence: Confidence = Confidence.HIGH
125
+
126
+ # Entry point transport category (language-agnostic).
127
+ # "http" for web routes, "cli" for CLI commands, "task" for async tasks, etc.
128
+ kind: str = "http"
129
+
130
+
131
+ # =============================================================================
132
+ # Dependency Injection Types
133
+ # =============================================================================
134
+
135
+
136
+ @dataclass
137
+ class ExtractedDependency:
138
+ """
139
+ A dependency injection definition extracted from framework code.
140
+
141
+ This captures FastAPI's Depends(), Spring's @Autowired, etc.
142
+ """
143
+
144
+ name: str
145
+ qualified_name: QualifiedName
146
+ location: CodeLocation
147
+
148
+ # Type of dependency
149
+ dependency_type: str # "function", "class", "provider", etc.
150
+
151
+ # What does this dependency provide?
152
+ provides_type: str | None = None
153
+ provides_qualified_name: QualifiedName | None = None
154
+
155
+ # Dependencies this depends on (chain)
156
+ depends_on: list[QualifiedName] = field(default_factory=list)
157
+
158
+ # Is this auth-related?
159
+ is_auth_related: bool = False
160
+ auth_scheme_ref: str | None = None
161
+
162
+ # Scope (singleton, request, etc.)
163
+ scope: str | None = None
164
+
165
+ # Analysis metadata
166
+ confidence: Confidence = Confidence.HIGH
167
+
168
+
169
+ # =============================================================================
170
+ # Authentication Types
171
+ # =============================================================================
172
+
173
+
174
+ @dataclass
175
+ class ExtractedAuthScheme:
176
+ """
177
+ An authentication scheme extracted from framework code.
178
+
179
+ This captures OAuth2 schemes, API key definitions, etc.
180
+ """
181
+
182
+ scheme_type: AuthSchemeType
183
+ name: str
184
+ location: CodeLocation
185
+
186
+ # Scheme configuration
187
+ config: dict[str, Any] = field(default_factory=dict)
188
+
189
+ # For OAuth2
190
+ token_url: str | None = None
191
+ authorization_url: str | None = None
192
+ scopes: dict[str, str] = field(default_factory=dict) # scope -> description
193
+
194
+ # For API keys
195
+ header_name: str | None = None
196
+ query_param_name: str | None = None
197
+ cookie_name: str | None = None
198
+
199
+ # Analysis metadata
200
+ confidence: Confidence = Confidence.HIGH
201
+
202
+
203
+ @dataclass
204
+ class ExtractedAuthDependency:
205
+ """
206
+ An authentication dependency/guard extracted from framework code.
207
+
208
+ This captures functions/classes that perform authentication.
209
+ """
210
+
211
+ name: str
212
+ qualified_name: QualifiedName
213
+ location: CodeLocation
214
+
215
+ dependency_type: AuthDependencyType
216
+
217
+ # What scheme(s) does this use?
218
+ uses_schemes: list[str] = field(default_factory=list)
219
+
220
+ # What other auth dependencies does this depend on?
221
+ depends_on: list[QualifiedName] = field(default_factory=list)
222
+
223
+ # What does this extract/validate?
224
+ extracts_fields: list[str] = field(default_factory=list)
225
+ validates: list[str] = field(default_factory=list)
226
+
227
+ # Role/scope requirements
228
+ requires_roles: list[str] = field(default_factory=list)
229
+ requires_scopes: list[str] = field(default_factory=list)
230
+
231
+ # JWT-specific operations
232
+ jwt_operations: list[str] = field(default_factory=list)
233
+
234
+ # Analysis metadata
235
+ confidence: Confidence = Confidence.HIGH
236
+
237
+
238
+ @dataclass
239
+ class ExtractedJwtConfig:
240
+ """JWT configuration extracted from code."""
241
+
242
+ library: str
243
+
244
+ detected: bool = False
245
+ locations: list[CodeLocation] = field(default_factory=list)
246
+
247
+ algorithms: list[str] = field(default_factory=list)
248
+
249
+ validates_signature: bool = False
250
+ validates_expiry: bool = False
251
+ validates_issuer: bool = False
252
+ validates_audience: bool = False
253
+
254
+ secret_source: str | None = None # "env", "config", "hardcoded"
255
+ secret_name: str | None = None
256
+
257
+ confidence: Confidence = Confidence.HIGH
258
+
259
+
260
+ # =============================================================================
261
+ # Middleware Types
262
+ # =============================================================================
263
+
264
+
265
+ @dataclass
266
+ class ExtractedMiddleware:
267
+ """
268
+ Middleware/interceptor extracted from framework code.
269
+
270
+ This captures FastAPI middleware, Spring filters/interceptors, etc.
271
+ """
272
+
273
+ name: str
274
+ location: CodeLocation
275
+
276
+ # Type
277
+ middleware_type: str # "middleware", "filter", "interceptor"
278
+
279
+ # Optional fields
280
+ qualified_name: QualifiedName | None = None
281
+
282
+ # Ordering
283
+ order: int | None = None
284
+
285
+ # What routes does this apply to?
286
+ applies_to_patterns: list[str] = field(default_factory=list)
287
+ applies_to_all: bool = False
288
+
289
+ # Detected operations
290
+ operations: list[str] = field(default_factory=list) # "auth", "cors", "logging", etc.
291
+
292
+ # Analysis metadata
293
+ confidence: Confidence = Confidence.HIGH
294
+
295
+
296
+ # =============================================================================
297
+ # Shared helpers (used by plugins across languages)
298
+ # =============================================================================
299
+
300
+
301
+ # Matches ``{name}`` and ``{name:constraint}`` route-template segments.
302
+ # Captures the parameter name only — the optional ``:constraint`` suffix is
303
+ # stripped. Works for Spring/Micronaut path variables and ASP.NET route
304
+ # templates ({id:int} → id, {slug:regex(...)} → slug).
305
+ _PATH_TEMPLATE_NAME_RE = re.compile(r"\{([^}:]+)(?::[^}]+)?\}")
306
+
307
+
308
+ def extract_path_template_names(path: str) -> set[str]:
309
+ """Return the set of parameter names embedded in a route template.
310
+
311
+ Strips any ``:constraint`` suffix on each segment::
312
+
313
+ "/api/users/{id}/orders/{orderId:int}"
314
+ → {"id", "orderId"}
315
+ """
316
+ return set(_PATH_TEMPLATE_NAME_RE.findall(path))
317
+
318
+
319
+ # Common middleware/filter "what does it do" keyword classifier.
320
+ # Maps an operation tag → list of substrings to look for in the lowercased
321
+ # (class-name + " " + base-class-names) combination. All three of Spring,
322
+ # Micronaut, and ASP.NET Core had their own near-identical version of this.
323
+ _MIDDLEWARE_OP_KEYWORDS: dict[str, tuple[str, ...]] = {
324
+ "auth": ("auth", "jwt", "token", "security", "login"),
325
+ "cors": ("cors",),
326
+ "logging": ("log",),
327
+ "rate_limiting": ("rate", "throttl"),
328
+ }
329
+
330
+
331
+ def infer_middleware_operations(
332
+ class_name: str,
333
+ base_classes: list[str] | tuple[str, ...] = (),
334
+ extra: dict[str, tuple[str, ...]] | None = None,
335
+ ) -> list[str]:
336
+ """Infer what a middleware/filter class does from its name and base classes.
337
+
338
+ Returns the matching operation tags (``["auth"]``, ``["cors", "logging"]``,
339
+ etc.), or ``["custom"]`` if nothing matches. Pass ``extra`` to add
340
+ framework-specific keywords (e.g. ``{"compression": ("compress", "gzip")}``
341
+ for Spring).
342
+ """
343
+ combined = class_name.lower() + " " + " ".join(b.lower() for b in base_classes)
344
+ keywords = {**_MIDDLEWARE_OP_KEYWORDS, **(extra or {})}
345
+ ops = [op for op, kws in keywords.items() if any(kw in combined for kw in kws)]
346
+ return ops or ["custom"]
347
+
348
+
349
+ # Class/symbol name heuristics for "is this auth-related?" — used by
350
+ # dependency extraction to flag service classes that handle authentication.
351
+ # This is the union of what Spring/Micronaut/.NET previously each carried
352
+ # their own copy of. ``identity`` and ``middleware`` are .NET-flavoured
353
+ # but harmless on Java code (Spring rarely names things "Middleware").
354
+ AUTH_NAME_KEYWORDS: frozenset[str] = frozenset(
355
+ {
356
+ "auth",
357
+ "authentication",
358
+ "authorization",
359
+ "security",
360
+ "jwt",
361
+ "token",
362
+ "user",
363
+ "identity",
364
+ "principal",
365
+ "credential",
366
+ "login",
367
+ "filter",
368
+ "middleware",
369
+ }
370
+ )
371
+
372
+
373
+ def is_auth_related_name(name: str) -> bool:
374
+ """True if a class/file/symbol name contains common auth-related substrings."""
375
+ name_lower = name.lower()
376
+ return any(kw in name_lower for kw in AUTH_NAME_KEYWORDS)
377
+
378
+
379
+ # =============================================================================
380
+ # Abstract Framework Plugin Base
381
+ # =============================================================================
382
+
383
+
384
+ class BaseFrameworkPlugin(ABC):
385
+ """
386
+ Abstract base class for framework-specific plugins.
387
+
388
+ Each framework (FastAPI, Spring Boot, etc.) extends this
389
+ to provide specialized extraction logic.
390
+ """
391
+
392
+ # Class-level constants to be overridden
393
+ FRAMEWORK: ClassVar[Framework]
394
+ LANGUAGE: ClassVar[Language]
395
+
396
+ # Import patterns that indicate this framework is used.
397
+ #
398
+ # - ``DETECTION_IMPORTS`` matches full module paths or ``module.symbol`` forms exactly.
399
+ # - ``DETECTION_IMPORT_PREFIXES`` matches the start of a module path — useful for
400
+ # namespace-rooted libraries where any subpackage import counts as detection
401
+ # (e.g. ``"org.springframework"``, ``"io.micronaut"``, ``"Microsoft.AspNetCore"``).
402
+ #
403
+ # Subclasses set at least one of the two; both are checked by the default
404
+ # ``detect()`` implementation. Subclasses with extra detection signals
405
+ # (e.g. attribute-only detection when imports are missing) override ``detect()``.
406
+ DETECTION_IMPORTS: ClassVar[frozenset[str]] = frozenset()
407
+ DETECTION_IMPORT_PREFIXES: ClassVar[tuple[str, ...]] = ()
408
+
409
+ @property
410
+ def framework(self) -> Framework:
411
+ """The framework this plugin handles."""
412
+ return self.FRAMEWORK
413
+
414
+ @property
415
+ def language(self) -> Language:
416
+ """The language this framework is for."""
417
+ return self.LANGUAGE
418
+
419
+ def detect(self, parsed_file: ParsedFile) -> bool:
420
+ """
421
+ Detect if this framework is used in the parsed file.
422
+
423
+ Default implementation checks each import against ``DETECTION_IMPORTS``
424
+ (exact match) and ``DETECTION_IMPORT_PREFIXES`` (prefix match).
425
+ Subclasses override only if they need extra signals beyond imports.
426
+ """
427
+ prefixes = self.DETECTION_IMPORT_PREFIXES
428
+ for imp in parsed_file.imports:
429
+ module = imp.module or ""
430
+ if module in self.DETECTION_IMPORTS:
431
+ return True
432
+ if prefixes and module.startswith(prefixes):
433
+ return True
434
+ for name in imp.names:
435
+ full_name = f"{module}.{name}" if module else name
436
+ if full_name in self.DETECTION_IMPORTS:
437
+ return True
438
+ return False
439
+
440
+ @abstractmethod
441
+ def extract_routes(
442
+ self,
443
+ parsed_file: ParsedFile,
444
+ context: AnalysisContext | None = None,
445
+ ) -> list[ExtractedRoute]:
446
+ """
447
+ Extract HTTP route definitions from the file.
448
+
449
+ Args:
450
+ parsed_file: The parsed source file
451
+ context: Optional analysis context with services
452
+
453
+ Returns:
454
+ List of extracted route definitions
455
+ """
456
+ ...
457
+
458
+ @abstractmethod
459
+ def extract_dependencies(self, parsed_file: ParsedFile) -> list[ExtractedDependency]:
460
+ """
461
+ Extract dependency injection definitions.
462
+
463
+ Args:
464
+ parsed_file: The parsed source file
465
+
466
+ Returns:
467
+ List of extracted dependency definitions
468
+ """
469
+ ...
470
+
471
+ @abstractmethod
472
+ def extract_auth_schemes(self, parsed_file: ParsedFile) -> list[ExtractedAuthScheme]:
473
+ """
474
+ Extract authentication scheme definitions.
475
+
476
+ Args:
477
+ parsed_file: The parsed source file
478
+
479
+ Returns:
480
+ List of extracted auth scheme definitions
481
+ """
482
+ ...
483
+
484
+ @abstractmethod
485
+ def extract_auth_dependencies(
486
+ self,
487
+ parsed_file: ParsedFile,
488
+ known_scheme_names: set[str] | None = None,
489
+ **kwargs: Any,
490
+ ) -> list[ExtractedAuthDependency]:
491
+ """
492
+ Extract authentication dependencies/guards.
493
+
494
+ Args:
495
+ parsed_file: The parsed source file
496
+ known_scheme_names: Optional set of auth scheme variable names
497
+ already extracted for this file, used to populate
498
+ ``uses_schemes`` on returned dependencies.
499
+
500
+ Returns:
501
+ List of extracted auth dependency definitions
502
+ """
503
+ ...
504
+
505
+ def extract_jwt_config(self, parsed_file: ParsedFile) -> ExtractedJwtConfig | None:
506
+ """
507
+ Extract JWT configuration.
508
+
509
+ Default implementation returns None. Override if the
510
+ framework has specific JWT patterns.
511
+ """
512
+ return None
513
+
514
+ @abstractmethod
515
+ def extract_middleware(self, parsed_file: ParsedFile) -> list[ExtractedMiddleware]:
516
+ """
517
+ Extract middleware/interceptor definitions.
518
+
519
+ Args:
520
+ parsed_file: The parsed source file
521
+
522
+ Returns:
523
+ List of extracted middleware definitions
524
+ """
525
+ ...
526
+
527
+ # ==========================================================================
528
+ # Helper Methods
529
+ # ==========================================================================
530
+
531
+ def _find_decorator(
532
+ self, function: ParsedFunction, decorator_names: set[str]
533
+ ) -> ParsedDecorator | None:
534
+ """Find a decorator by name on a function."""
535
+ for dec in function.decorators:
536
+ if dec.name in decorator_names:
537
+ return dec
538
+ # Check qualified name
539
+ if dec.qualified_name and dec.qualified_name.name in decorator_names:
540
+ return dec
541
+ return None
542
+
543
+ def _has_decorator(self, function: ParsedFunction, decorator_names: set[str]) -> bool:
544
+ """Check if function has any of the given decorators."""
545
+ return self._find_decorator(function, decorator_names) is not None
546
+
547
+ def _extract_decorator_arg(
548
+ self, decorator: ParsedDecorator, arg_name: str, default: Any = None
549
+ ) -> Any:
550
+ """Extract an argument value from a decorator."""
551
+ return decorator.arguments.get(arg_name, default)
552
+
553
+
554
+ # =============================================================================
555
+ # Framework Plugin Registry
556
+ # =============================================================================
557
+
558
+
559
+ class FrameworkPluginRegistry:
560
+ """
561
+ Registry of available framework plugins.
562
+
563
+ Plugins register themselves and the registry provides
564
+ lookup and detection capabilities.
565
+ """
566
+
567
+ _plugins: dict[Framework, BaseFrameworkPlugin] = {}
568
+ _by_language: dict[Language, list[BaseFrameworkPlugin]] = {}
569
+
570
+ @classmethod
571
+ def register(cls, plugin: BaseFrameworkPlugin) -> None:
572
+ """Register a framework plugin."""
573
+ cls._plugins[plugin.framework] = plugin
574
+
575
+ if plugin.language not in cls._by_language:
576
+ cls._by_language[plugin.language] = []
577
+ cls._by_language[plugin.language].append(plugin)
578
+
579
+ @classmethod
580
+ def get_plugin(cls, framework: Framework) -> BaseFrameworkPlugin | None:
581
+ """Get plugin for a specific framework."""
582
+ return cls._plugins.get(framework)
583
+
584
+ @classmethod
585
+ def get_plugins_for_language(cls, language: Language) -> list[BaseFrameworkPlugin]:
586
+ """Get all plugins for a language."""
587
+ return cls._by_language.get(language, [])
588
+
589
+ @classmethod
590
+ def detect_frameworks(cls, parsed_file: ParsedFile) -> list[Framework]:
591
+ """
592
+ Detect which frameworks are used in a parsed file.
593
+
594
+ Returns list of detected frameworks, may be empty.
595
+ """
596
+ detected = []
597
+ plugins = cls.get_plugins_for_language(parsed_file.language)
598
+
599
+ for plugin in plugins:
600
+ if plugin.detect(parsed_file):
601
+ detected.append(plugin.framework)
602
+
603
+ return detected
604
+
605
+ @classmethod
606
+ def supported_frameworks(cls) -> frozenset[Framework]:
607
+ """Get set of supported frameworks."""
608
+ return frozenset(cls._plugins.keys())
@@ -0,0 +1,17 @@
1
+ """ASP.NET framework plugins — auto-registers on import."""
2
+
3
+ from .aspnet_plugin import AspNetCorePlugin
4
+ from .grpc_plugin import GrpcPlugin
5
+ from .jwt_config_extractor import DotNetJwtConfigExtractor
6
+ from .legacy_aspnet_plugin import LegacyAspNetPlugin
7
+ from .refit_plugin import RefitPlugin
8
+ from .wcf_plugin import WcfPlugin
9
+
10
+ __all__ = [
11
+ "AspNetCorePlugin",
12
+ "DotNetJwtConfigExtractor",
13
+ "GrpcPlugin",
14
+ "LegacyAspNetPlugin",
15
+ "RefitPlugin",
16
+ "WcfPlugin",
17
+ ]
@@ -0,0 +1,43 @@
1
+ """
2
+ Shared .NET routing-path helpers.
3
+
4
+ Both AspNetCorePlugin and RefitPlugin need to:
5
+ - Extract the path string from a `[Route("...")]`-style attribute, supporting
6
+ positional and named arguments
7
+ - Strip ASP.NET-style route-template constraints (e.g. `{id:int}` → `{id}`)
8
+
9
+ Previously RefitPlugin instantiated an AspNetCorePlugin just to reach these
10
+ helpers — that's an architectural smell since Refit is *not* an MVC plugin.
11
+ Promoting both functions to module-level removes the indirection.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+
18
+ from ...parsing.base import ParsedDecorator
19
+
20
+
21
+ def dec_path(dec: ParsedDecorator) -> str | None:
22
+ """Extract the path string from a routing attribute.
23
+
24
+ Tries positional arg 0 first, then the named keys ASP.NET attribute
25
+ routing accepts (`template` / `Template` / `name` / `Name`).
26
+ """
27
+ if dec.positional_args:
28
+ val = dec.positional_args[0]
29
+ if isinstance(val, str):
30
+ return val
31
+ for key in ("template", "Template", "name", "Name"):
32
+ val = dec.arguments.get(key)
33
+ if val and isinstance(val, str):
34
+ return val
35
+ return None
36
+
37
+
38
+ _ROUTE_CONSTRAINT_RE = re.compile(r"\{([^}:]+):[^}]+\}")
39
+
40
+
41
+ def normalize_path(path: str) -> str:
42
+ """Strip ASP.NET route-constraint suffixes: `{id:int}` → `{id}`, `{slug:regex(...)}` → `{slug}`."""
43
+ return _ROUTE_CONSTRAINT_RE.sub(r"{\1}", path)