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,361 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared JWT configuration extractor for JVM-based framework plugins.
|
|
3
|
+
|
|
4
|
+
Used by SpringBootPlugin and MicronautPlugin. The design is intentionally
|
|
5
|
+
self-contained so that a parallel DotNetJwtConfigExtractor can follow the
|
|
6
|
+
same structure for ASP.NET Core.
|
|
7
|
+
|
|
8
|
+
Detection strategy
|
|
9
|
+
------------------
|
|
10
|
+
1. Library — identified from import prefixes.
|
|
11
|
+
2. Algorithms — extracted from signWith() / Algorithm.HMAC256() call arguments
|
|
12
|
+
and enum member references (SignatureAlgorithm.HS256).
|
|
13
|
+
3. Validation flags (signature, expiry, issuer, audience) — inferred from
|
|
14
|
+
builder-chain method names and Spring Security defaults.
|
|
15
|
+
4. Secret source — "env" from System.getenv(), "config" from @Value("${...}")
|
|
16
|
+
on fields, "hardcoded" from literal strings passed to signing methods.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from ...parsing.base import ParsedDecorator, ParsedFile
|
|
25
|
+
from .._jwt_common import add_algorithm as _add_algorithm
|
|
26
|
+
from .._jwt_common import normalize_algorithm as _normalize_alg
|
|
27
|
+
from ..base import ExtractedJwtConfig
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Library detection
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
# Import module prefix → canonical library name surfaced in the manifest
|
|
34
|
+
_JWT_IMPORT_PREFIXES: dict[str, str] = {
|
|
35
|
+
"io.jsonwebtoken": "jjwt",
|
|
36
|
+
"com.nimbusds.jwt": "nimbus-jose-jwt",
|
|
37
|
+
"com.nimbusds.jose": "nimbus-jose-jwt",
|
|
38
|
+
"org.springframework.security.oauth2.jwt": "spring-security-oauth2",
|
|
39
|
+
"com.auth0.jwt": "auth0-java-jwt",
|
|
40
|
+
"org.jose4j": "jose4j",
|
|
41
|
+
"io.micronaut.security.token.jwt": "micronaut-security-jwt",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Individual class names (may arrive via wildcard imports or direct imports)
|
|
45
|
+
_JWT_CLASS_NAMES: frozenset[str] = frozenset(
|
|
46
|
+
{
|
|
47
|
+
"JwtDecoder",
|
|
48
|
+
"JwtEncoder",
|
|
49
|
+
"NimbusJwtDecoder",
|
|
50
|
+
"NimbusJwtEncoder",
|
|
51
|
+
"JwtParser",
|
|
52
|
+
"JwtParserBuilder",
|
|
53
|
+
"JwtBuilder", # jjwt
|
|
54
|
+
"SignedJWT",
|
|
55
|
+
"JWTClaimsSet", # Nimbus
|
|
56
|
+
"DecodedJWT",
|
|
57
|
+
"JWTVerifier", # Auth0
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Algorithm normalization
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
# Java-library-specific algorithm name aliases → JWA canonical name.
|
|
66
|
+
# The canonical JWA set lives in `frameworks/_jwt_common.py`.
|
|
67
|
+
_ALGORITHM_ALIASES: dict[str, str] = {
|
|
68
|
+
# jjwt SignatureAlgorithm / MacAlgorithm enum values
|
|
69
|
+
"HMACSHA256": "HS256",
|
|
70
|
+
"HMACSHA384": "HS384",
|
|
71
|
+
"HMACSHA512": "HS512",
|
|
72
|
+
# Auth0 Java JWT
|
|
73
|
+
"HMAC256": "HS256",
|
|
74
|
+
"HMAC384": "HS384",
|
|
75
|
+
"HMAC512": "HS512",
|
|
76
|
+
"RSA256": "RS256",
|
|
77
|
+
"RSA384": "RS384",
|
|
78
|
+
"RSA512": "RS512",
|
|
79
|
+
"ECDSA256": "ES256",
|
|
80
|
+
"ECDSA384": "ES384",
|
|
81
|
+
"ECDSA512": "ES512",
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# Call site keyword sets
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
_PARSE_KEYWORDS = frozenset(
|
|
89
|
+
{
|
|
90
|
+
"parsesignedclaims",
|
|
91
|
+
"parseclaimsjws",
|
|
92
|
+
"parsejwt",
|
|
93
|
+
"parsewithhint",
|
|
94
|
+
"decode",
|
|
95
|
+
"verify",
|
|
96
|
+
"validate",
|
|
97
|
+
"parsejwsstring",
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
_SIGN_KEYWORDS = frozenset({"signwith", "withalgorithm", "algorithm"})
|
|
102
|
+
|
|
103
|
+
_EXPIRY_KEYWORDS = frozenset(
|
|
104
|
+
{
|
|
105
|
+
"requireexpirationtime",
|
|
106
|
+
"acceptleeway",
|
|
107
|
+
"timestampvalidator",
|
|
108
|
+
"expiresat",
|
|
109
|
+
"setexpiration",
|
|
110
|
+
"clockskew",
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
_ISSUER_KEYWORDS = frozenset(
|
|
115
|
+
{
|
|
116
|
+
"requireissuer",
|
|
117
|
+
"setissuer",
|
|
118
|
+
"withissuer",
|
|
119
|
+
"issuer",
|
|
120
|
+
"validateissuer",
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
_AUDIENCE_KEYWORDS = frozenset(
|
|
125
|
+
{
|
|
126
|
+
"requireaudience",
|
|
127
|
+
"setaudience",
|
|
128
|
+
"withaudience",
|
|
129
|
+
"audience",
|
|
130
|
+
"validateaudience",
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
# Secret detection
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
_SECRET_KEYWORDS: frozenset[str] = frozenset(
|
|
139
|
+
{
|
|
140
|
+
"jwt",
|
|
141
|
+
"secret",
|
|
142
|
+
"signing",
|
|
143
|
+
"signkey",
|
|
144
|
+
"token",
|
|
145
|
+
"key",
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
_VALUE_RE = re.compile(r"[$#]\{([^}]+)\}")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _normalize_algorithm(raw: str) -> str | None:
|
|
153
|
+
"""Return JWA algorithm name or None if unrecognised."""
|
|
154
|
+
return _normalize_alg(raw, _ALGORITHM_ALIASES)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _value_dec_raw(dec: ParsedDecorator) -> str | None:
|
|
158
|
+
"""Extract the raw annotation string from an @Value decorator."""
|
|
159
|
+
if dec.positional_args:
|
|
160
|
+
return str(dec.positional_args[0])
|
|
161
|
+
val = dec.arguments.get("value")
|
|
162
|
+
return str(val) if val else None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
# Main extractor
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class JavaJwtConfigExtractor:
|
|
171
|
+
"""
|
|
172
|
+
Extracts JWT configuration from a single parsed JVM file.
|
|
173
|
+
|
|
174
|
+
Framework plugins instantiate this once and call ``extract(parsed_file)``.
|
|
175
|
+
Subclass and override ``EXTRA_IMPORT_PREFIXES`` to add framework-specific
|
|
176
|
+
library prefixes without duplicating the core logic.
|
|
177
|
+
|
|
178
|
+
.NET equivalent: ``DotNetJwtConfigExtractor`` (same structure, different
|
|
179
|
+
library names and attribute syntax).
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
# Framework-specific additions (override in subclasses if needed)
|
|
183
|
+
EXTRA_IMPORT_PREFIXES: dict[str, str] = {}
|
|
184
|
+
|
|
185
|
+
def extract(self, parsed_file: ParsedFile) -> ExtractedJwtConfig | None: # noqa: C901
|
|
186
|
+
jwt_config = ExtractedJwtConfig(library="", detected=False)
|
|
187
|
+
|
|
188
|
+
# ── 1. Library detection ──────────────────────────────────────────────
|
|
189
|
+
all_prefixes = {**_JWT_IMPORT_PREFIXES, **self.EXTRA_IMPORT_PREFIXES}
|
|
190
|
+
|
|
191
|
+
for imp in parsed_file.imports:
|
|
192
|
+
module = imp.module or ""
|
|
193
|
+
for prefix, lib_name in all_prefixes.items():
|
|
194
|
+
if module.startswith(prefix):
|
|
195
|
+
jwt_config.detected = True
|
|
196
|
+
jwt_config.library = jwt_config.library or lib_name
|
|
197
|
+
break
|
|
198
|
+
|
|
199
|
+
# Also detect well-known JWT class names imported individually
|
|
200
|
+
for name in imp.names:
|
|
201
|
+
if name in _JWT_CLASS_NAMES and not jwt_config.detected:
|
|
202
|
+
jwt_config.detected = True
|
|
203
|
+
jwt_config.library = jwt_config.library or "spring-security-oauth2"
|
|
204
|
+
|
|
205
|
+
if not jwt_config.detected:
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
# Spring Security and Micronaut Security validate signature + expiry
|
|
209
|
+
# by default when a JwtDecoder / JwtValidator bean is configured.
|
|
210
|
+
if any(kw in jwt_config.library for kw in ("spring-security", "micronaut-security")):
|
|
211
|
+
jwt_config.validates_signature = True
|
|
212
|
+
jwt_config.validates_expiry = True
|
|
213
|
+
|
|
214
|
+
# ── 2. Call site analysis ─────────────────────────────────────────────
|
|
215
|
+
for call in parsed_file.call_sites:
|
|
216
|
+
callee = call.callee_name.lower()
|
|
217
|
+
|
|
218
|
+
# Parse / decode / verify → JWT is being consumed here
|
|
219
|
+
if any(kw in callee for kw in _PARSE_KEYWORDS):
|
|
220
|
+
jwt_config.locations.append(call.location)
|
|
221
|
+
jwt_config.validates_signature = True
|
|
222
|
+
|
|
223
|
+
# Algorithm detection
|
|
224
|
+
if any(kw in callee for kw in _SIGN_KEYWORDS):
|
|
225
|
+
for arg in call.arguments:
|
|
226
|
+
self._collect_algorithm_from_arg(arg, jwt_config)
|
|
227
|
+
|
|
228
|
+
# Also detect Algorithm.HMAC256(...) / Algorithm.RSA256(...) as the
|
|
229
|
+
# direct callee when the signing call itself IS the algorithm factory
|
|
230
|
+
# e.g. JWT.require(Algorithm.HMAC256("secret"))
|
|
231
|
+
algo_from_callee = _normalize_algorithm(call.callee_name.split(".")[-1])
|
|
232
|
+
if algo_from_callee:
|
|
233
|
+
_add_algorithm(algo_from_callee, jwt_config)
|
|
234
|
+
|
|
235
|
+
# Validation flags
|
|
236
|
+
if any(kw in callee for kw in _EXPIRY_KEYWORDS):
|
|
237
|
+
jwt_config.validates_expiry = True
|
|
238
|
+
if any(kw in callee for kw in _ISSUER_KEYWORDS):
|
|
239
|
+
jwt_config.validates_issuer = True
|
|
240
|
+
if any(kw in callee for kw in _AUDIENCE_KEYWORDS):
|
|
241
|
+
jwt_config.validates_audience = True
|
|
242
|
+
|
|
243
|
+
# Secret source: System.getenv("JWT_SECRET")
|
|
244
|
+
if "getenv" in callee and jwt_config.secret_source is None:
|
|
245
|
+
for arg in call.arguments:
|
|
246
|
+
if arg.is_literal and isinstance(arg.literal_value, str):
|
|
247
|
+
val = arg.literal_value
|
|
248
|
+
if any(kw in val.lower() for kw in _SECRET_KEYWORDS):
|
|
249
|
+
jwt_config.secret_source = "env"
|
|
250
|
+
jwt_config.secret_name = val
|
|
251
|
+
|
|
252
|
+
# Secret source: hardcoded string in signWith(Keys.hmacShaKeyFor(secret.getBytes()))
|
|
253
|
+
# where secret itself is a literal — detect via signWith + literal in args
|
|
254
|
+
if "signwith" in callee and jwt_config.secret_source is None:
|
|
255
|
+
for arg in call.arguments:
|
|
256
|
+
if arg.is_literal and isinstance(arg.literal_value, str):
|
|
257
|
+
# A literal key string passed directly is hardcoded
|
|
258
|
+
if len(arg.literal_value) >= 8: # skip empty/trivial
|
|
259
|
+
jwt_config.secret_source = "hardcoded"
|
|
260
|
+
|
|
261
|
+
# ── 3. @Value field annotations → config secret ───────────────────────
|
|
262
|
+
if jwt_config.secret_source is None:
|
|
263
|
+
self._scan_value_annotations(parsed_file, jwt_config)
|
|
264
|
+
|
|
265
|
+
# ── 4. Field initializer expressions (System.getenv in field init) ────
|
|
266
|
+
if jwt_config.secret_source is None:
|
|
267
|
+
self._scan_field_initializers(parsed_file, jwt_config)
|
|
268
|
+
|
|
269
|
+
return jwt_config
|
|
270
|
+
|
|
271
|
+
def _collect_algorithm_from_arg(self, arg: Any, jwt_config: ExtractedJwtConfig) -> None:
|
|
272
|
+
"""Try to extract a JWA algorithm name from a single call argument.
|
|
273
|
+
|
|
274
|
+
Handles all four storage forms produced by the Java parser:
|
|
275
|
+
- ``variable_name`` — ``SignatureAlgorithm.HS256`` → stored as
|
|
276
|
+
``is_variable=True, variable_name="HS256"`` (qualifier lost)
|
|
277
|
+
- ``called_function`` — ``Algorithm.HMAC256("secret")`` passed as an
|
|
278
|
+
argument → ``is_call_result=True, called_function="HMAC256"``
|
|
279
|
+
- ``expression_text`` — fallback raw text from the AST
|
|
280
|
+
- ``literal_value`` — bare string literals like ``"HS256"``
|
|
281
|
+
"""
|
|
282
|
+
# MemberReference: SignatureAlgorithm.HS256 → variable_name="HS256"
|
|
283
|
+
var = getattr(arg, "variable_name", None)
|
|
284
|
+
if var:
|
|
285
|
+
normalized = _normalize_algorithm(var)
|
|
286
|
+
if normalized:
|
|
287
|
+
_add_algorithm(normalized, jwt_config)
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
# MethodInvocation as argument: Algorithm.HMAC256("secret") →
|
|
291
|
+
# called_function="HMAC256"
|
|
292
|
+
called = getattr(arg, "called_function", None)
|
|
293
|
+
if called:
|
|
294
|
+
normalized = _normalize_algorithm(called.split(".")[-1])
|
|
295
|
+
if normalized:
|
|
296
|
+
_add_algorithm(normalized, jwt_config)
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
# Expression text fallback (may contain the full "SignatureAlgorithm.HS256")
|
|
300
|
+
expr = getattr(arg, "expression_text", None)
|
|
301
|
+
if expr:
|
|
302
|
+
for token in re.split(r"[.\s,()\[\]]+", expr):
|
|
303
|
+
normalized = _normalize_algorithm(token)
|
|
304
|
+
if normalized:
|
|
305
|
+
_add_algorithm(normalized, jwt_config)
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
# Bare literal string, e.g. "HS256"
|
|
309
|
+
if getattr(arg, "is_literal", False):
|
|
310
|
+
val = getattr(arg, "literal_value", None)
|
|
311
|
+
if isinstance(val, str):
|
|
312
|
+
normalized = _normalize_algorithm(val)
|
|
313
|
+
if normalized:
|
|
314
|
+
_add_algorithm(normalized, jwt_config)
|
|
315
|
+
|
|
316
|
+
def _scan_field_initializers(
|
|
317
|
+
self,
|
|
318
|
+
parsed_file: ParsedFile,
|
|
319
|
+
jwt_config: ExtractedJwtConfig,
|
|
320
|
+
) -> None:
|
|
321
|
+
"""Detect System.getenv("JWT_SECRET") stored in field initializers.
|
|
322
|
+
|
|
323
|
+
``String secret = System.getenv("JWT_SECRET")`` is a field declaration,
|
|
324
|
+
not a statement, so it doesn't appear in ``call_sites``. We scan field
|
|
325
|
+
``default_value`` strings directly using a regex.
|
|
326
|
+
"""
|
|
327
|
+
_getenv_re = re.compile(r'getenv\s*\(\s*["\']([^"\']+)["\']\s*\)')
|
|
328
|
+
for cls in parsed_file.classes:
|
|
329
|
+
for f in cls.fields:
|
|
330
|
+
dv = getattr(f, "default_value", None)
|
|
331
|
+
if not dv:
|
|
332
|
+
continue
|
|
333
|
+
m = _getenv_re.search(dv)
|
|
334
|
+
if m:
|
|
335
|
+
val = m.group(1)
|
|
336
|
+
if any(kw in val.lower() for kw in _SECRET_KEYWORDS):
|
|
337
|
+
jwt_config.secret_source = "env"
|
|
338
|
+
jwt_config.secret_name = val
|
|
339
|
+
return
|
|
340
|
+
|
|
341
|
+
def _scan_value_annotations(
|
|
342
|
+
self,
|
|
343
|
+
parsed_file: ParsedFile,
|
|
344
|
+
jwt_config: ExtractedJwtConfig,
|
|
345
|
+
) -> None:
|
|
346
|
+
"""Detect @Value("${jwt.secret}") on class fields → secret_source="config"."""
|
|
347
|
+
for cls in parsed_file.classes:
|
|
348
|
+
for f in cls.fields:
|
|
349
|
+
for dec in getattr(f, "decorators", None) or []:
|
|
350
|
+
if dec.name != "Value":
|
|
351
|
+
continue
|
|
352
|
+
raw = _value_dec_raw(dec)
|
|
353
|
+
if not raw:
|
|
354
|
+
continue
|
|
355
|
+
m = _VALUE_RE.search(raw)
|
|
356
|
+
if m:
|
|
357
|
+
key = m.group(1)
|
|
358
|
+
if any(kw in key.lower() for kw in _SECRET_KEYWORDS):
|
|
359
|
+
jwt_config.secret_source = "config"
|
|
360
|
+
jwt_config.secret_name = key
|
|
361
|
+
return # first match wins
|