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,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)
|