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,545 @@
|
|
|
1
|
+
"""
|
|
2
|
+
.NET JWT configuration extractor for ASP.NET Core framework plugin.
|
|
3
|
+
|
|
4
|
+
Mirrors the structure of JavaJwtConfigExtractor so the two can be maintained
|
|
5
|
+
in parallel. Detection strategy:
|
|
6
|
+
|
|
7
|
+
1. Library — identified from using/import prefixes.
|
|
8
|
+
2. Algorithms — extracted from SecurityAlgorithms.HmacSha256 references,
|
|
9
|
+
TokenValidationParameters.ValidAlgorithms assignments, and
|
|
10
|
+
string literals matching known JWA names.
|
|
11
|
+
3. Validation flags — inferred from TokenValidationParameters property
|
|
12
|
+
assignments (ValidateIssuerSigningKey, ValidateLifetime,
|
|
13
|
+
ValidateIssuer, ValidateAudience).
|
|
14
|
+
4. Secret source — "env" from Environment.GetEnvironmentVariable(),
|
|
15
|
+
"config" from IConfiguration["jwt:secret"] / appsettings keys,
|
|
16
|
+
"hardcoded" from literal strings passed to signing helpers or
|
|
17
|
+
stored in string fields whose names contain a secret keyword.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from ...parsing.base import ParsedFile
|
|
26
|
+
from .._jwt_common import add_algorithm as _add_algorithm
|
|
27
|
+
from .._jwt_common import normalize_algorithm as _normalize_alg
|
|
28
|
+
from ..base import ExtractedJwtConfig
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Library detection
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
_JWT_IMPORT_PREFIXES: dict[str, str] = {
|
|
35
|
+
"Microsoft.IdentityModel.Tokens": "microsoft-identity-model",
|
|
36
|
+
"System.IdentityModel.Tokens.Jwt": "system-identitymodel-jwt",
|
|
37
|
+
"Microsoft.AspNetCore.Authentication.JwtBearer": "aspnetcore-jwt-bearer",
|
|
38
|
+
"Microsoft.AspNetCore.Authorization": "aspnetcore-authorization",
|
|
39
|
+
"JWT": "jwt-net", # JWT.net (joakimskoog/jwt)
|
|
40
|
+
"Jose": "jose-jwt", # jose-jwt
|
|
41
|
+
"System.Security.Cryptography": "dotnet-crypto",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_JWT_CLASS_NAMES: frozenset[str] = frozenset(
|
|
45
|
+
{
|
|
46
|
+
"JwtSecurityToken",
|
|
47
|
+
"JwtSecurityTokenHandler",
|
|
48
|
+
"SecurityToken",
|
|
49
|
+
"TokenValidationParameters",
|
|
50
|
+
"JwtBearerOptions",
|
|
51
|
+
"JwtRegisteredClaimNames",
|
|
52
|
+
"SigningCredentials",
|
|
53
|
+
"SymmetricSecurityKey",
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# Algorithm aliases (canonical JWA set lives in frameworks/_jwt_common.py)
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
# SecurityAlgorithms constants → JWA name
|
|
62
|
+
_ALGORITHM_ALIASES: dict[str, str] = {
|
|
63
|
+
# HMAC-SHA variants
|
|
64
|
+
"HMACSHA256": "HS256",
|
|
65
|
+
"HMACSHA384": "HS384",
|
|
66
|
+
"HMACSHA512": "HS512",
|
|
67
|
+
# SecurityAlgorithms static field names
|
|
68
|
+
"HMACSHA256SIGNATURE": "HS256",
|
|
69
|
+
"HMACSHA384SIGNATURE": "HS384",
|
|
70
|
+
"HMACSHA512SIGNATURE": "HS512",
|
|
71
|
+
"RSASHA256": "RS256",
|
|
72
|
+
"RSASHA384": "RS384",
|
|
73
|
+
"RSASHA512": "RS512",
|
|
74
|
+
"RSASSHA256": "RS256",
|
|
75
|
+
"ECDSASHA256": "ES256",
|
|
76
|
+
"ECDSASHA384": "ES384",
|
|
77
|
+
"ECDSASHA512": "ES512",
|
|
78
|
+
# String forms sometimes used directly
|
|
79
|
+
"HTTP://WWW.W3.ORG/2001/04/XMLDSIG-MORE#HMAC-SHA256": "HS256",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
# Call-site keyword sets
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
_PARSE_KEYWORDS: frozenset[str] = frozenset(
|
|
87
|
+
{
|
|
88
|
+
"validatetoken",
|
|
89
|
+
"readtoken",
|
|
90
|
+
"readjwttoken",
|
|
91
|
+
"canreadtoken",
|
|
92
|
+
"validatejwt",
|
|
93
|
+
"decode",
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
_SIGN_KEYWORDS: frozenset[str] = frozenset(
|
|
98
|
+
{
|
|
99
|
+
"signingcredentials",
|
|
100
|
+
"addsigningkey",
|
|
101
|
+
"withalgorithm",
|
|
102
|
+
"setsigningkey",
|
|
103
|
+
"signingkey",
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
_EXPIRY_KEYWORDS: frozenset[str] = frozenset(
|
|
108
|
+
{
|
|
109
|
+
"validatelifetime",
|
|
110
|
+
"clockskew",
|
|
111
|
+
"setexpiry",
|
|
112
|
+
"notbefore",
|
|
113
|
+
"expires",
|
|
114
|
+
"expiresat",
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
_ISSUER_KEYWORDS: frozenset[str] = frozenset(
|
|
119
|
+
{
|
|
120
|
+
"validateissuer",
|
|
121
|
+
"validissuer",
|
|
122
|
+
"validissuers",
|
|
123
|
+
"issuer",
|
|
124
|
+
"setissuer",
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
_AUDIENCE_KEYWORDS: frozenset[str] = frozenset(
|
|
129
|
+
{
|
|
130
|
+
"validateaudience",
|
|
131
|
+
"validaudience",
|
|
132
|
+
"validaudiences",
|
|
133
|
+
"audience",
|
|
134
|
+
"setaudience",
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
# Secret detection
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
_SECRET_KEYWORDS: frozenset[str] = frozenset(
|
|
143
|
+
{
|
|
144
|
+
"jwt",
|
|
145
|
+
"secret",
|
|
146
|
+
"signing",
|
|
147
|
+
"signkey",
|
|
148
|
+
"token",
|
|
149
|
+
"key",
|
|
150
|
+
"password",
|
|
151
|
+
"credential",
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
_VALUE_RE = re.compile(r'["\']((?:jwt|secret|signing|key|token)[^"\']{0,40})["\']', re.IGNORECASE)
|
|
156
|
+
_CONFIG_RE = re.compile(
|
|
157
|
+
r'(?:configuration|config|_config|IConfiguration)\s*(?:\[|\.GetValue[^(]*\()\s*["\']([^"\']+)["\']',
|
|
158
|
+
re.IGNORECASE,
|
|
159
|
+
)
|
|
160
|
+
_GETENV_RE = re.compile(r'GetEnvironmentVariable\s*\(\s*["\']([^"\']+)["\']\s*\)')
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _normalize_algorithm(raw: str) -> str | None:
|
|
164
|
+
"""Return JWA algorithm name or None if unrecognised."""
|
|
165
|
+
return _normalize_alg(raw, _ALGORITHM_ALIASES)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
# Main extractor
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class DotNetJwtConfigExtractor:
|
|
174
|
+
"""
|
|
175
|
+
Extracts JWT configuration from a single parsed C# file.
|
|
176
|
+
|
|
177
|
+
ASP.NET Core plugin instantiates this once and calls ``extract(parsed_file)``.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
EXTRA_IMPORT_PREFIXES: dict[str, str] = {}
|
|
181
|
+
|
|
182
|
+
def extract(self, parsed_file: ParsedFile) -> ExtractedJwtConfig | None: # noqa: C901
|
|
183
|
+
jwt_config = ExtractedJwtConfig(library="", detected=False)
|
|
184
|
+
|
|
185
|
+
# ── 1. Library detection ──────────────────────────────────────────
|
|
186
|
+
all_prefixes = {**_JWT_IMPORT_PREFIXES, **self.EXTRA_IMPORT_PREFIXES}
|
|
187
|
+
|
|
188
|
+
for imp in parsed_file.imports:
|
|
189
|
+
module = imp.module or ""
|
|
190
|
+
for prefix, lib_name in all_prefixes.items():
|
|
191
|
+
if module.startswith(prefix):
|
|
192
|
+
jwt_config.detected = True
|
|
193
|
+
jwt_config.library = jwt_config.library or lib_name
|
|
194
|
+
break
|
|
195
|
+
|
|
196
|
+
for name in imp.names:
|
|
197
|
+
if name in _JWT_CLASS_NAMES and not jwt_config.detected:
|
|
198
|
+
jwt_config.detected = True
|
|
199
|
+
jwt_config.library = jwt_config.library or "aspnetcore-jwt-bearer"
|
|
200
|
+
|
|
201
|
+
if not jwt_config.detected:
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
# ASP.NET Core JWT bearer validates signature + expiry by default
|
|
205
|
+
if "aspnetcore" in jwt_config.library or "microsoft-identity" in jwt_config.library:
|
|
206
|
+
jwt_config.validates_signature = True
|
|
207
|
+
jwt_config.validates_expiry = True
|
|
208
|
+
|
|
209
|
+
# ── 2. Call-site analysis ─────────────────────────────────────────
|
|
210
|
+
for call in parsed_file.call_sites:
|
|
211
|
+
callee = call.callee_name.lower()
|
|
212
|
+
|
|
213
|
+
# Parse / validate → JWT is consumed here
|
|
214
|
+
if any(kw in callee for kw in _PARSE_KEYWORDS):
|
|
215
|
+
jwt_config.locations.append(call.location)
|
|
216
|
+
jwt_config.validates_signature = True
|
|
217
|
+
|
|
218
|
+
# Algorithm detection via method name
|
|
219
|
+
if any(kw in callee for kw in _SIGN_KEYWORDS):
|
|
220
|
+
# Collect algo from callee suffix (e.g. "SecurityAlgorithms.HmacSha256")
|
|
221
|
+
algo = _normalize_algorithm(call.callee_name.split(".")[-1])
|
|
222
|
+
if algo:
|
|
223
|
+
_add_algorithm(algo, jwt_config)
|
|
224
|
+
|
|
225
|
+
# SecurityAlgorithms.HmacSha256 used as a standalone reference
|
|
226
|
+
algo_from_callee = _normalize_algorithm(call.callee_name.split(".")[-1])
|
|
227
|
+
if algo_from_callee:
|
|
228
|
+
_add_algorithm(algo_from_callee, jwt_config)
|
|
229
|
+
|
|
230
|
+
# Validation flags
|
|
231
|
+
if any(kw in callee for kw in _EXPIRY_KEYWORDS):
|
|
232
|
+
jwt_config.validates_expiry = True
|
|
233
|
+
if any(kw in callee for kw in _ISSUER_KEYWORDS):
|
|
234
|
+
jwt_config.validates_issuer = True
|
|
235
|
+
if any(kw in callee for kw in _AUDIENCE_KEYWORDS):
|
|
236
|
+
jwt_config.validates_audience = True
|
|
237
|
+
|
|
238
|
+
# Environment variable secret
|
|
239
|
+
if "getenvironmentvariable" in callee and jwt_config.secret_source is None:
|
|
240
|
+
pass # handled in field scan below via regex
|
|
241
|
+
|
|
242
|
+
# ── 3. Field / property annotation scan ──────────────────────────
|
|
243
|
+
if jwt_config.secret_source is None:
|
|
244
|
+
self._scan_fields(parsed_file, jwt_config)
|
|
245
|
+
|
|
246
|
+
# ── 4. Static / const string fields with literal values ───────────
|
|
247
|
+
if jwt_config.secret_source is None:
|
|
248
|
+
self._scan_static_fields(parsed_file, jwt_config)
|
|
249
|
+
|
|
250
|
+
# ── 5. TokenValidationParameters property assignments in field text ─
|
|
251
|
+
self._scan_validation_assignments(parsed_file, jwt_config)
|
|
252
|
+
|
|
253
|
+
# ── 6. SecurityAlgorithms member references (field/property, not calls) ─
|
|
254
|
+
self._scan_security_algorithms_refs(parsed_file, jwt_config)
|
|
255
|
+
|
|
256
|
+
# ── 7. AddJwtBearer(config => { ... }) lambda bodies ────────────────
|
|
257
|
+
# In minimal-hosting Program.cs the JWT config sits at module scope,
|
|
258
|
+
# outside any class. The C# parser captures the AddJwtBearer call
|
|
259
|
+
# site but doesn't expose the lambda body — read it from disk and
|
|
260
|
+
# apply the same regex patterns the in-method scan uses.
|
|
261
|
+
self._scan_addjwtbearer_lambda_bodies(parsed_file, jwt_config)
|
|
262
|
+
|
|
263
|
+
return jwt_config
|
|
264
|
+
|
|
265
|
+
# ------------------------------------------------------------------
|
|
266
|
+
# Helpers
|
|
267
|
+
# ------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
def _scan_fields(self, parsed_file: ParsedFile, jwt_config: ExtractedJwtConfig) -> None:
|
|
270
|
+
"""
|
|
271
|
+
Scan field default values for:
|
|
272
|
+
- Environment.GetEnvironmentVariable("JWT_SECRET")
|
|
273
|
+
- configuration["jwt:secret"] / _config.GetValue<string>("...")
|
|
274
|
+
"""
|
|
275
|
+
for cls in parsed_file.classes:
|
|
276
|
+
for f in cls.fields:
|
|
277
|
+
dv = getattr(f, "default_value", None) or ""
|
|
278
|
+
|
|
279
|
+
# Environment variable
|
|
280
|
+
m = _GETENV_RE.search(dv)
|
|
281
|
+
if m:
|
|
282
|
+
val = m.group(1)
|
|
283
|
+
if any(kw in val.lower() for kw in _SECRET_KEYWORDS):
|
|
284
|
+
jwt_config.secret_source = "env"
|
|
285
|
+
jwt_config.secret_name = val
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
# Configuration key
|
|
289
|
+
m2 = _CONFIG_RE.search(dv)
|
|
290
|
+
if m2:
|
|
291
|
+
key = m2.group(1)
|
|
292
|
+
if any(kw in key.lower() for kw in _SECRET_KEYWORDS):
|
|
293
|
+
jwt_config.secret_source = "config"
|
|
294
|
+
jwt_config.secret_name = key
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
# Also check [FromConfiguration] / [SecretName] decorators on fields
|
|
298
|
+
for f in cls.fields:
|
|
299
|
+
for dec in getattr(f, "decorators", None) or []:
|
|
300
|
+
if dec.name in ("ConfigurationValue", "SecretName", "Value"):
|
|
301
|
+
raw = (
|
|
302
|
+
dec.positional_args[0]
|
|
303
|
+
if dec.positional_args
|
|
304
|
+
else (dec.arguments.get("value") or dec.arguments.get("Value"))
|
|
305
|
+
)
|
|
306
|
+
if raw and isinstance(raw, str):
|
|
307
|
+
if any(kw in raw.lower() for kw in _SECRET_KEYWORDS):
|
|
308
|
+
jwt_config.secret_source = "config"
|
|
309
|
+
jwt_config.secret_name = raw
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
def _scan_static_fields(self, parsed_file: ParsedFile, jwt_config: ExtractedJwtConfig) -> None:
|
|
313
|
+
"""Detect private const/static string fields with literal secret values.
|
|
314
|
+
|
|
315
|
+
The C# parser strips quote characters from string literals (via
|
|
316
|
+
_string_literal_value), so default_value contains the bare content
|
|
317
|
+
without surrounding quotes. We accept both quoted (Java-style raw
|
|
318
|
+
default_value) and unquoted (C# parser output) forms.
|
|
319
|
+
|
|
320
|
+
A value is treated as a literal — not an expression — when it contains
|
|
321
|
+
no parentheses (which would indicate a function call) and no ``${``
|
|
322
|
+
(Spring-style placeholder).
|
|
323
|
+
"""
|
|
324
|
+
_quoted_re = re.compile(r'^["\'](.{4,})["\']$')
|
|
325
|
+
for cls in parsed_file.classes:
|
|
326
|
+
for f in cls.fields:
|
|
327
|
+
if not any(kw in (f.name or "").lower() for kw in _SECRET_KEYWORDS):
|
|
328
|
+
continue
|
|
329
|
+
dv = (getattr(f, "default_value", None) or "").strip()
|
|
330
|
+
if not dv or len(dv) < 4:
|
|
331
|
+
continue
|
|
332
|
+
# Quoted form (Java parser keeps surrounding quotes)
|
|
333
|
+
if _quoted_re.match(dv):
|
|
334
|
+
jwt_config.secret_source = "hardcoded"
|
|
335
|
+
jwt_config.secret_name = f.name
|
|
336
|
+
return
|
|
337
|
+
# Unquoted form (C# parser already stripped quotes) — exclude
|
|
338
|
+
# anything that looks like an expression (function call, property
|
|
339
|
+
# access placeholder, etc.)
|
|
340
|
+
if "(" not in dv and "${" not in dv:
|
|
341
|
+
jwt_config.secret_source = "hardcoded"
|
|
342
|
+
jwt_config.secret_name = f.name
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
# SecurityAlgorithms.HmacSha256 / SecurityAlgorithms.RsaSha256 are field
|
|
346
|
+
# references, not method calls — they don't appear in call_sites.
|
|
347
|
+
_SECURITY_ALGO_RE = re.compile(
|
|
348
|
+
r"SecurityAlgorithms\."
|
|
349
|
+
r"(Hmac(?:Sha256|Sha384|Sha512)"
|
|
350
|
+
r"|Rsa(?:Sha256|Sha384|Sha512)"
|
|
351
|
+
r"|EcDsa(?:Sha256|Sha384|Sha512)"
|
|
352
|
+
r"|(?:HS|RS|ES|PS)(?:256|384|512))",
|
|
353
|
+
re.IGNORECASE,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# Human-readable SecurityAlgorithms field name → JWA
|
|
357
|
+
_SEC_ALGO_MAP: dict[str, str] = {
|
|
358
|
+
"hmacsha256": "HS256",
|
|
359
|
+
"hmacsha384": "HS384",
|
|
360
|
+
"hmacsha512": "HS512",
|
|
361
|
+
"rsasha256": "RS256",
|
|
362
|
+
"rsasha384": "RS384",
|
|
363
|
+
"rsasha512": "RS512",
|
|
364
|
+
"ecdsasha256": "ES256",
|
|
365
|
+
"ecdsasha384": "ES384",
|
|
366
|
+
"ecdsasha512": "ES512",
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
def _scan_security_algorithms_refs(
|
|
370
|
+
self, parsed_file: ParsedFile, jwt_config: ExtractedJwtConfig
|
|
371
|
+
) -> None:
|
|
372
|
+
"""
|
|
373
|
+
Scan field initializers and method call names for SecurityAlgorithms.*
|
|
374
|
+
references that indicate which signing algorithm is in use.
|
|
375
|
+
|
|
376
|
+
``SecurityAlgorithms.HmacSha256`` is a constant string field, not a
|
|
377
|
+
method — it won't appear in ``call_sites``. We match it with a regex
|
|
378
|
+
over field default_values and the call site callee names.
|
|
379
|
+
"""
|
|
380
|
+
# Check call_sites: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
|
|
381
|
+
# appears as callee "SecurityAlgorithms.HmacSha256" when captured via
|
|
382
|
+
# object_creation_expression arguments (future enhancement) or via the
|
|
383
|
+
# callee of an invocation. Also check for the pattern in callee names.
|
|
384
|
+
for call in parsed_file.call_sites:
|
|
385
|
+
m = self._SECURITY_ALGO_RE.search(call.callee_name)
|
|
386
|
+
if m:
|
|
387
|
+
raw = m.group(1).lower().replace("_", "")
|
|
388
|
+
algo = self._SEC_ALGO_MAP.get(raw) or _normalize_algorithm(raw)
|
|
389
|
+
if algo:
|
|
390
|
+
_add_algorithm(algo, jwt_config)
|
|
391
|
+
|
|
392
|
+
# Check field default values (e.g. const string algo = SecurityAlgorithms.HmacSha256)
|
|
393
|
+
for cls in parsed_file.classes:
|
|
394
|
+
for f in cls.fields:
|
|
395
|
+
dv = getattr(f, "default_value", None) or ""
|
|
396
|
+
m = self._SECURITY_ALGO_RE.search(dv)
|
|
397
|
+
if m:
|
|
398
|
+
raw = m.group(1).lower().replace("_", "")
|
|
399
|
+
algo = self._SEC_ALGO_MAP.get(raw) or _normalize_algorithm(raw)
|
|
400
|
+
if algo:
|
|
401
|
+
_add_algorithm(algo, jwt_config)
|
|
402
|
+
|
|
403
|
+
def _scan_validation_assignments(
|
|
404
|
+
self, parsed_file: ParsedFile, jwt_config: ExtractedJwtConfig
|
|
405
|
+
) -> None:
|
|
406
|
+
"""
|
|
407
|
+
Look for TokenValidationParameters property assignments in method bodies.
|
|
408
|
+
|
|
409
|
+
Example patterns we detect:
|
|
410
|
+
ValidateIssuerSigningKey = true
|
|
411
|
+
ValidateLifetime = false
|
|
412
|
+
ValidAlgorithms = new[] { SecurityAlgorithms.HmacSha256 }
|
|
413
|
+
"""
|
|
414
|
+
_bool_assign_re = re.compile(
|
|
415
|
+
r"(Validate(?:IssuerSigningKey|Lifetime|Issuer|Audience|Actor))\s*=\s*(true|false)",
|
|
416
|
+
re.IGNORECASE,
|
|
417
|
+
)
|
|
418
|
+
_algo_assign_re = re.compile(
|
|
419
|
+
r"ValidAlgorithms\s*=\s*new[^{]*\{([^}]+)\}",
|
|
420
|
+
re.IGNORECASE,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
for cls in parsed_file.classes:
|
|
424
|
+
for method in cls.methods:
|
|
425
|
+
body = getattr(method, "body_source", None) or ""
|
|
426
|
+
if not body:
|
|
427
|
+
continue
|
|
428
|
+
|
|
429
|
+
for match in _bool_assign_re.finditer(body):
|
|
430
|
+
prop = match.group(1).lower()
|
|
431
|
+
val = match.group(2).lower() == "true"
|
|
432
|
+
if "issuerSigningKey" in prop or "issuersigningkey" in prop.lower():
|
|
433
|
+
if val:
|
|
434
|
+
jwt_config.validates_signature = True
|
|
435
|
+
elif "lifetime" in prop:
|
|
436
|
+
if val:
|
|
437
|
+
jwt_config.validates_expiry = True
|
|
438
|
+
else:
|
|
439
|
+
jwt_config.validates_expiry = False
|
|
440
|
+
elif "issuer" in prop:
|
|
441
|
+
if val:
|
|
442
|
+
jwt_config.validates_issuer = True
|
|
443
|
+
elif "audience" in prop:
|
|
444
|
+
if val:
|
|
445
|
+
jwt_config.validates_audience = True
|
|
446
|
+
|
|
447
|
+
m2 = _algo_assign_re.search(body)
|
|
448
|
+
if m2:
|
|
449
|
+
for token in re.split(r'[\s,."]+', m2.group(1)):
|
|
450
|
+
algo = _normalize_algorithm(token)
|
|
451
|
+
if algo:
|
|
452
|
+
_add_algorithm(algo, jwt_config)
|
|
453
|
+
|
|
454
|
+
# Brace-balanced extraction of the lambda body for AddJwtBearer(config => { ... }).
|
|
455
|
+
_SYMMETRIC_KEY_RE = re.compile(r"new\s+SymmetricSecurityKey\s*\(\s*([A-Za-z_][\w\.]*)\s*\)")
|
|
456
|
+
_BOOL_PROP_RE = re.compile(
|
|
457
|
+
r"(Validate(?:IssuerSigningKey|Lifetime|Issuer|Audience|Actor))\s*=\s*(true|false)",
|
|
458
|
+
re.IGNORECASE,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
def _scan_addjwtbearer_lambda_bodies(
|
|
462
|
+
self, parsed_file: ParsedFile, jwt_config: ExtractedJwtConfig
|
|
463
|
+
) -> None:
|
|
464
|
+
"""
|
|
465
|
+
Find every `AddJwtBearer(config => { ... })` call and parse the lambda
|
|
466
|
+
body for validation flags and signing-key references.
|
|
467
|
+
|
|
468
|
+
Minimal-hosting Program.cs has the JWT config at module scope —
|
|
469
|
+
outside any class — so the in-method body scan in
|
|
470
|
+
_scan_validation_assignments misses it entirely. We read the file
|
|
471
|
+
from disk, walk forward from the call's line, and brace-match to
|
|
472
|
+
extract the lambda body.
|
|
473
|
+
"""
|
|
474
|
+
# Only fire when we have AddJwtBearer call sites
|
|
475
|
+
jwt_calls = [
|
|
476
|
+
c for c in parsed_file.call_sites if c.callee_name.rsplit(".", 1)[-1] == "AddJwtBearer"
|
|
477
|
+
]
|
|
478
|
+
if not jwt_calls:
|
|
479
|
+
return
|
|
480
|
+
|
|
481
|
+
try:
|
|
482
|
+
source = Path(parsed_file.path).read_text(encoding="utf-8", errors="ignore")
|
|
483
|
+
except OSError:
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
for call in jwt_calls:
|
|
487
|
+
body = self._extract_lambda_body(source, call.location.line)
|
|
488
|
+
if not body:
|
|
489
|
+
continue
|
|
490
|
+
|
|
491
|
+
# Boolean validation flags inside TokenValidationParameters initializer.
|
|
492
|
+
for m in self._BOOL_PROP_RE.finditer(body):
|
|
493
|
+
prop = m.group(1).lower()
|
|
494
|
+
val = m.group(2).lower() == "true"
|
|
495
|
+
if "issuersigningkey" in prop:
|
|
496
|
+
if val:
|
|
497
|
+
jwt_config.validates_signature = True
|
|
498
|
+
elif "lifetime" in prop:
|
|
499
|
+
jwt_config.validates_expiry = val
|
|
500
|
+
elif "issuer" in prop:
|
|
501
|
+
jwt_config.validates_issuer = val
|
|
502
|
+
elif "audience" in prop:
|
|
503
|
+
jwt_config.validates_audience = val
|
|
504
|
+
|
|
505
|
+
# IssuerSigningKey = new SymmetricSecurityKey(key) — capture the
|
|
506
|
+
# referenced variable name so downstream tooling can trace it back
|
|
507
|
+
# to its declaration (often `var key = Encoding.ASCII.GetBytes(...)`).
|
|
508
|
+
if jwt_config.secret_source is None:
|
|
509
|
+
m = self._SYMMETRIC_KEY_RE.search(body)
|
|
510
|
+
if m:
|
|
511
|
+
jwt_config.secret_source = "code"
|
|
512
|
+
jwt_config.secret_name = m.group(1)
|
|
513
|
+
|
|
514
|
+
@staticmethod
|
|
515
|
+
def _extract_lambda_body(source: str, start_line: int) -> str:
|
|
516
|
+
"""
|
|
517
|
+
Return the lambda body of an `AddJwtBearer(config => { ... })` call
|
|
518
|
+
located at-or-after `start_line` (1-indexed).
|
|
519
|
+
|
|
520
|
+
The parser records the line of the *first* statement in a chained
|
|
521
|
+
invocation (`builder.Services.AddAuthentication(...).AddJwtBearer(...)`
|
|
522
|
+
gets line=55, the AddAuthentication line, not the AddJwtBearer line).
|
|
523
|
+
We compensate by locating the literal `AddJwtBearer` token in the
|
|
524
|
+
forward chunk first, then brace-matching from there.
|
|
525
|
+
"""
|
|
526
|
+
lines = source.splitlines()
|
|
527
|
+
if start_line < 1 or start_line > len(lines):
|
|
528
|
+
return ""
|
|
529
|
+
chunk = "\n".join(lines[start_line - 1 : start_line - 1 + 200])
|
|
530
|
+
marker = chunk.find("AddJwtBearer")
|
|
531
|
+
if marker == -1:
|
|
532
|
+
return ""
|
|
533
|
+
# Find the opening brace of the lambda body that follows.
|
|
534
|
+
brace = chunk.find("{", marker)
|
|
535
|
+
if brace == -1:
|
|
536
|
+
return ""
|
|
537
|
+
depth = 0
|
|
538
|
+
for i, ch in enumerate(chunk[brace:], start=brace):
|
|
539
|
+
if ch == "{":
|
|
540
|
+
depth += 1
|
|
541
|
+
elif ch == "}":
|
|
542
|
+
depth -= 1
|
|
543
|
+
if depth == 0:
|
|
544
|
+
return chunk[brace : i + 1]
|
|
545
|
+
return ""
|