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,427 @@
1
+ """
2
+ Click and Typer CLI framework plugin.
3
+
4
+ Detects CLI command entry points from Click and Typer decorator patterns.
5
+
6
+ Supported constructs:
7
+ - @click.command() / @click.group() — standalone commands and groups
8
+ - @app.command() / @grp.command() — commands registered on groups
9
+ - @click.option() / @click.argument() — parameter decorators
10
+ - @click.password_option() — sensitive input patterns
11
+ - @click.pass_context / @click.pass_obj — context injection (skipped in params)
12
+ - Typer @app.command() / @app.callback() — Typer command patterns
13
+ - Typer function parameter defaults (typer.Option, typer.Argument)
14
+
15
+ Not supported (yet):
16
+ - Dynamic command registration via group.add_command()
17
+ - click.MultiCommand subclasses
18
+ - Lazy-loaded command groups
19
+ - Complex group nesting hierarchy reconstruction
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import re
25
+ from typing import Any
26
+
27
+ from ...core.types import (
28
+ Confidence,
29
+ Framework,
30
+ HttpMethod,
31
+ Language,
32
+ ParameterLocation,
33
+ )
34
+ from ...parsing.base import ParsedDecorator, ParsedFile, ParsedFunction, ParsedParameter
35
+ from ..base import (
36
+ BaseFrameworkPlugin,
37
+ ExtractedAuthDependency,
38
+ ExtractedAuthScheme,
39
+ ExtractedDependency,
40
+ ExtractedMiddleware,
41
+ ExtractedParameter,
42
+ ExtractedResponse,
43
+ ExtractedRoute,
44
+ FrameworkPluginRegistry,
45
+ )
46
+
47
+ CLICK_IMPORTS = frozenset({"click", "typer"})
48
+
49
+ # Decorators that mark a function as a CLI command entry point.
50
+ COMMAND_DECORATORS: set[str] = {
51
+ "command",
52
+ "group",
53
+ "callback",
54
+ "click.command",
55
+ "click.group",
56
+ "typer.command",
57
+ }
58
+
59
+ # Decorators that define CLI parameters (options/arguments).
60
+ OPTION_DECORATORS: set[str] = {
61
+ "option",
62
+ "click.option",
63
+ "password_option",
64
+ "click.password_option",
65
+ }
66
+
67
+ ARGUMENT_DECORATORS: set[str] = {
68
+ "argument",
69
+ "click.argument",
70
+ }
71
+
72
+ # Decorators that inject Click context objects — the first function parameter
73
+ # they introduce should be skipped during parameter extraction.
74
+ CONTEXT_DECORATORS: set[str] = {
75
+ "pass_context",
76
+ "click.pass_context",
77
+ "pass_obj",
78
+ "click.pass_obj",
79
+ }
80
+
81
+ # Suffixes stripped from function names when deriving the CLI command path.
82
+ _COMMAND_SUFFIXES = ("_command", "_cmd", "_group", "_grp")
83
+
84
+ _CLI_ARGUMENT_LOCATION = ParameterLocation.CLI_ARGUMENT
85
+ _CLI_OPTION_LOCATION = ParameterLocation.CLI_OPTION
86
+
87
+ _STRIP_DASHES_RE = re.compile(r"^-+")
88
+
89
+
90
+ def _derive_command_name(func_name: str) -> str:
91
+ """Derive a CLI command name from a Python function name.
92
+
93
+ Applies Click's default naming convention: lowercase, underscores become
94
+ dashes, and common command/group suffixes are stripped.
95
+ """
96
+ name = func_name.lower()
97
+ for suffix in _COMMAND_SUFFIXES:
98
+ if name.endswith(suffix):
99
+ name = name[: -len(suffix)]
100
+ break
101
+ return name.replace("_", "-")
102
+
103
+
104
+ def _option_name_from_decls(decls: list[Any]) -> str:
105
+ """Extract the canonical option name from param_decls.
106
+
107
+ Click param_decls are like ("--verbose", "-v"). We pick the longest
108
+ long-option string, strip leading dashes, and normalise.
109
+ """
110
+ long_opts = [_STRIP_DASHES_RE.sub("", str(d)) for d in decls if str(d).startswith("--")]
111
+ if long_opts:
112
+ return max(long_opts, key=len).replace("-", "_")
113
+ # Fall back to the first positional decl.
114
+ if decls:
115
+ return _STRIP_DASHES_RE.sub("", str(decls[0])).replace("-", "_")
116
+ return "unknown"
117
+
118
+
119
+ def _argument_name_from_decls(decls: list[Any]) -> str:
120
+ """Extract argument name from param_decls (usually a single positional)."""
121
+ if decls:
122
+ return str(decls[0]).strip().replace("-", "_")
123
+ return "unknown"
124
+
125
+
126
+ class ClickPlugin(BaseFrameworkPlugin):
127
+ """Plugin for Click and Typer CLI framework extraction."""
128
+
129
+ FRAMEWORK = Framework.CLICK
130
+ LANGUAGE = Language.PYTHON
131
+ DETECTION_IMPORTS = CLICK_IMPORTS
132
+
133
+ # ------------------------------------------------------------------
134
+ # Detection
135
+ # ------------------------------------------------------------------
136
+
137
+ def detect(self, parsed_file: ParsedFile) -> bool:
138
+ for imp in parsed_file.imports:
139
+ mod = (imp.module or "").split(".")[0]
140
+ if mod in ("click", "typer"):
141
+ return True
142
+ for name in imp.names:
143
+ if name in ("click", "typer", "Typer"):
144
+ return True
145
+ return False
146
+
147
+ # ------------------------------------------------------------------
148
+ # Route / command extraction
149
+ # ------------------------------------------------------------------
150
+
151
+ def extract_routes(
152
+ self,
153
+ parsed_file: ParsedFile,
154
+ context: Any = None,
155
+ ) -> list[ExtractedRoute]:
156
+ routes: list[ExtractedRoute] = []
157
+
158
+ for func in parsed_file.functions:
159
+ route = self._try_extract_command(func)
160
+ if route:
161
+ routes.append(route)
162
+
163
+ for cls in parsed_file.classes:
164
+ for method in cls.methods:
165
+ route = self._try_extract_command(method)
166
+ if route:
167
+ routes.append(route)
168
+
169
+ return routes
170
+
171
+ # ------------------------------------------------------------------
172
+ # Stubs — CLI frameworks have no HTTP-level dependency/auth/middleware
173
+ # ------------------------------------------------------------------
174
+
175
+ def extract_dependencies(self, parsed_file: ParsedFile) -> list[ExtractedDependency]:
176
+ return []
177
+
178
+ def extract_auth_schemes(self, parsed_file: ParsedFile) -> list[ExtractedAuthScheme]:
179
+ return []
180
+
181
+ def extract_auth_dependencies(
182
+ self,
183
+ parsed_file: ParsedFile,
184
+ known_scheme_names: set[str] | None = None,
185
+ **kwargs: Any,
186
+ ) -> list[ExtractedAuthDependency]:
187
+ return []
188
+
189
+ def extract_middleware(self, parsed_file: ParsedFile) -> list[ExtractedMiddleware]:
190
+ return []
191
+
192
+ # ------------------------------------------------------------------
193
+ # Internal helpers
194
+ # ------------------------------------------------------------------
195
+
196
+ def _try_extract_command(self, func: ParsedFunction) -> ExtractedRoute | None:
197
+ """If *func* is decorated as a Click/Typer command, return an ExtractedRoute."""
198
+ cmd_dec = self._find_command_decorator(func)
199
+ if cmd_dec is None:
200
+ return None
201
+
202
+ command_name = self._resolve_command_name(cmd_dec, func.name)
203
+ has_ctx = self._has_context_decorator(func)
204
+ params = self._extract_parameters(func, has_ctx)
205
+
206
+ return ExtractedRoute(
207
+ kind="cli",
208
+ method=HttpMethod.GET,
209
+ path=command_name,
210
+ handler_function=func.qualified_name,
211
+ handler_location=func.location,
212
+ query_params=[p for p in params if p.location == _CLI_OPTION_LOCATION],
213
+ path_params=[p for p in params if p.location == _CLI_ARGUMENT_LOCATION],
214
+ header_params=[],
215
+ cookie_params=[],
216
+ body=None,
217
+ response=ExtractedResponse(),
218
+ confidence=Confidence.HIGH,
219
+ )
220
+
221
+ def _find_command_decorator(self, func: ParsedFunction) -> ParsedDecorator | None:
222
+ """Return the first command/group/callback decorator on *func*."""
223
+ for dec in func.decorators:
224
+ base = self._decorator_base_name(dec)
225
+ if base in COMMAND_DECORATORS:
226
+ return dec
227
+ return None
228
+
229
+ def _has_context_decorator(self, func: ParsedFunction) -> bool:
230
+ for dec in func.decorators:
231
+ base = self._decorator_base_name(dec)
232
+ if base in CONTEXT_DECORATORS:
233
+ return True
234
+ return False
235
+
236
+ def _resolve_command_name(self, dec: ParsedDecorator, func_name: str) -> str:
237
+ """Get the CLI command path from the decorator or function name."""
238
+ explicit = self._extract_decorator_arg(dec, "name")
239
+ if explicit and isinstance(explicit, str):
240
+ return explicit
241
+ if dec.positional_args:
242
+ first = dec.positional_args[0]
243
+ if isinstance(first, str) and not first.startswith("-"):
244
+ return first
245
+ return _derive_command_name(func_name)
246
+
247
+ # ---- parameter extraction ----
248
+
249
+ def _extract_parameters(
250
+ self,
251
+ func: ParsedFunction,
252
+ has_ctx: bool,
253
+ ) -> list[ExtractedParameter]:
254
+ """Extract CLI parameters from decorators and function signature."""
255
+ params: list[ExtractedParameter] = []
256
+ decorator_param_names: set[str] = set()
257
+
258
+ for dec in func.decorators:
259
+ base = self._decorator_base_name(dec)
260
+
261
+ if base in OPTION_DECORATORS:
262
+ ep = self._param_from_option_decorator(dec, base)
263
+ params.append(ep)
264
+ decorator_param_names.add(ep.name)
265
+
266
+ elif base in ARGUMENT_DECORATORS:
267
+ ep = self._param_from_argument_decorator(dec)
268
+ params.append(ep)
269
+ decorator_param_names.add(ep.name)
270
+
271
+ # Infer remaining function parameters not covered by decorators.
272
+ params.extend(
273
+ self._infer_params_from_signature(
274
+ func.parameters,
275
+ decorator_param_names,
276
+ has_ctx,
277
+ )
278
+ )
279
+
280
+ return params
281
+
282
+ def _param_from_option_decorator(
283
+ self,
284
+ dec: ParsedDecorator,
285
+ base_name: str,
286
+ ) -> ExtractedParameter:
287
+ if base_name in ("password_option", "click.password_option"):
288
+ name = (
289
+ _option_name_from_decls(dec.positional_args) if dec.positional_args else "password"
290
+ )
291
+ else:
292
+ name = (
293
+ _option_name_from_decls(dec.positional_args) if dec.positional_args else "unknown"
294
+ )
295
+
296
+ required = dec.arguments.get("required", False)
297
+ default = dec.arguments.get("default")
298
+ type_hint = dec.arguments.get("type")
299
+
300
+ return ExtractedParameter(
301
+ name=name,
302
+ location=_CLI_OPTION_LOCATION,
303
+ type_annotation=str(type_hint) if type_hint is not None else None,
304
+ required=bool(required),
305
+ default_value=str(default) if default is not None else None,
306
+ )
307
+
308
+ def _param_from_argument_decorator(self, dec: ParsedDecorator) -> ExtractedParameter:
309
+ name = _argument_name_from_decls(dec.positional_args) if dec.positional_args else "unknown"
310
+ required = dec.arguments.get("required", True)
311
+ default = dec.arguments.get("default")
312
+ type_hint = dec.arguments.get("type")
313
+
314
+ return ExtractedParameter(
315
+ name=name,
316
+ location=_CLI_ARGUMENT_LOCATION,
317
+ type_annotation=str(type_hint) if type_hint is not None else None,
318
+ required=bool(required),
319
+ default_value=str(default) if default is not None else None,
320
+ )
321
+
322
+ def _infer_params_from_signature(
323
+ self,
324
+ parameters: list[ParsedParameter],
325
+ already_extracted: set[str],
326
+ has_ctx: bool,
327
+ ) -> list[ExtractedParameter]:
328
+ """Infer CLI params from the function signature.
329
+
330
+ Handles Typer-style defaults (``typer.Option(...)``, ``typer.Argument(...)``).
331
+ """
332
+ results: list[ExtractedParameter] = []
333
+ skip_first = has_ctx
334
+
335
+ for param in parameters:
336
+ if param.is_variadic or param.is_keyword_variadic:
337
+ continue
338
+ if skip_first:
339
+ skip_first = False
340
+ continue
341
+ if param.name in already_extracted:
342
+ continue
343
+ if param.name in ("self", "cls"):
344
+ continue
345
+
346
+ default = param.default_value
347
+ if default and "typer.Option" in default:
348
+ results.append(
349
+ ExtractedParameter(
350
+ name=param.name,
351
+ location=_CLI_OPTION_LOCATION,
352
+ type_annotation=param.type_annotation,
353
+ required=False,
354
+ default_value=None,
355
+ )
356
+ )
357
+ elif default and "typer.Argument" in default or default is None:
358
+ results.append(
359
+ ExtractedParameter(
360
+ name=param.name,
361
+ location=_CLI_ARGUMENT_LOCATION,
362
+ type_annotation=param.type_annotation,
363
+ required=True,
364
+ default_value=None,
365
+ )
366
+ )
367
+ else:
368
+ results.append(
369
+ ExtractedParameter(
370
+ name=param.name,
371
+ location=_CLI_OPTION_LOCATION,
372
+ type_annotation=param.type_annotation,
373
+ required=False,
374
+ default_value=default,
375
+ )
376
+ )
377
+
378
+ return results
379
+
380
+ # ---- decorator name helpers ----
381
+
382
+ @staticmethod
383
+ def _decorator_base_name(dec: ParsedDecorator) -> str:
384
+ """Normalise a decorator to a lookup key.
385
+
386
+ Handles both ``click.command`` (qualified) and ``command`` (bare import)
387
+ as well as ``app.command`` / ``grp.group`` attribute forms.
388
+ """
389
+ if dec.qualified_name:
390
+ qn = dec.qualified_name.full
391
+ parts = qn.rsplit(".", 1)
392
+ if len(parts) == 2:
393
+ mod, attr = parts
394
+ if mod in ("click", "typer"):
395
+ return f"{mod}.{attr}"
396
+ if attr in (
397
+ "command",
398
+ "group",
399
+ "callback",
400
+ "option",
401
+ "argument",
402
+ "password_option",
403
+ "pass_context",
404
+ "pass_obj",
405
+ ):
406
+ return attr
407
+ return qn
408
+
409
+ name = dec.name
410
+ if "." in name:
411
+ _obj, _, attr = name.rpartition(".")
412
+ if attr in (
413
+ "command",
414
+ "group",
415
+ "callback",
416
+ "option",
417
+ "argument",
418
+ "password_option",
419
+ "pass_context",
420
+ "pass_obj",
421
+ ):
422
+ return attr
423
+ return name
424
+
425
+
426
+ _click_plugin = ClickPlugin()
427
+ FrameworkPluginRegistry.register(_click_plugin)