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