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,393 @@
1
+ """
2
+ Task framework plugin for Celery, Dramatiq, Huey, and RQ.
3
+
4
+ Detects async task entry points from decorator patterns across the four
5
+ most widely-used Python task/worker frameworks.
6
+
7
+ Supported constructs — Celery (5.x):
8
+ - @app.task / @app.task(name="...", bind=True, ...)
9
+ - @shared_task / @shared_task(name="...", ...)
10
+ - @celery.task (alternate Celery instance naming)
11
+ - Class-based tasks (subclasses of celery.Task) — NOT yet supported
12
+
13
+ Supported constructs — Dramatiq (2.x):
14
+ - @dramatiq.actor / @dramatiq.actor(queue_name="...", actor_name="...", ...)
15
+
16
+ Supported constructs — Huey (2.x):
17
+ - @huey.task() / @huey.periodic_task(...)
18
+ - Alternate instance names: @queue.task(), @queue.periodic_task()
19
+
20
+ Supported constructs — RQ (1.x):
21
+ - @job(queue, connection, ...) from rq.decorators
22
+
23
+ Not supported (yet):
24
+ - Celery class-based tasks (subclass of celery.Task with run() method)
25
+ - Celery canvas primitives (chain, group, chord as entry points)
26
+ - Dynamic task registration (app.register_task)
27
+ - Dramatiq middleware hooks
28
+ - Huey pre/post-execute hooks
29
+ - RQ queue.enqueue() calls as entry points
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ from typing import Any, ClassVar
35
+
36
+ from ...core.types import (
37
+ Confidence,
38
+ Framework,
39
+ HttpMethod,
40
+ Language,
41
+ ParameterLocation,
42
+ )
43
+ from ...parsing.base import ParsedDecorator, ParsedFile, ParsedFunction
44
+ from ..base import (
45
+ BaseFrameworkPlugin,
46
+ ExtractedAuthDependency,
47
+ ExtractedAuthScheme,
48
+ ExtractedDependency,
49
+ ExtractedMiddleware,
50
+ ExtractedParameter,
51
+ ExtractedResponse,
52
+ ExtractedRoute,
53
+ FrameworkPluginRegistry,
54
+ )
55
+
56
+ # ── Import detection ────────────────────────────────────────────────────────
57
+
58
+ CELERY_IMPORTS = frozenset(
59
+ {
60
+ "celery",
61
+ "celery.app",
62
+ "celery.shared_task",
63
+ }
64
+ )
65
+
66
+ DRAMATIQ_IMPORTS = frozenset(
67
+ {
68
+ "dramatiq",
69
+ }
70
+ )
71
+
72
+ HUEY_IMPORTS = frozenset(
73
+ {
74
+ "huey",
75
+ }
76
+ )
77
+
78
+ RQ_IMPORTS = frozenset(
79
+ {
80
+ "rq",
81
+ "rq.decorators",
82
+ }
83
+ )
84
+
85
+ ALL_TASK_IMPORTS = CELERY_IMPORTS | DRAMATIQ_IMPORTS | HUEY_IMPORTS | RQ_IMPORTS
86
+
87
+
88
+ # ── Decorator name sets ─────────────────────────────────────────────────────
89
+
90
+ # Celery: @app.task, @celery_app.task, @shared_task
91
+ CELERY_TASK_DECORATORS: set[str] = {
92
+ "task", # @app.task / @celery.task (attribute form)
93
+ "shared_task", # @shared_task (bare import from celery)
94
+ "celery.shared_task", # @celery.shared_task (qualified)
95
+ }
96
+
97
+ # Dramatiq: @dramatiq.actor
98
+ DRAMATIQ_ACTOR_DECORATORS: set[str] = {
99
+ "actor", # @broker.actor (attribute) or @dramatiq.actor (qualified)
100
+ "dramatiq.actor", # @dramatiq.actor
101
+ }
102
+
103
+ # Huey: @huey.task(), @huey.periodic_task()
104
+ HUEY_TASK_DECORATORS: set[str] = {
105
+ "task", # @huey.task() (attribute)
106
+ "periodic_task", # @huey.periodic_task()
107
+ }
108
+
109
+ # RQ: @job(queue, connection)
110
+ RQ_JOB_DECORATORS: set[str] = {
111
+ "job", # @job (from rq.decorators import job)
112
+ "rq.decorators.job", # qualified import
113
+ }
114
+
115
+ ALL_TASK_DECORATORS = (
116
+ CELERY_TASK_DECORATORS | DRAMATIQ_ACTOR_DECORATORS | HUEY_TASK_DECORATORS | RQ_JOB_DECORATORS
117
+ )
118
+
119
+
120
+ # ── Which framework set detected? ──────────────────────────────────────────
121
+
122
+
123
+ class _DetectedFramework:
124
+ """Tracks which task framework(s) are present in a file."""
125
+
126
+ __slots__ = ("celery", "dramatiq", "huey", "rq")
127
+
128
+ def __init__(self) -> None:
129
+ self.celery = False
130
+ self.dramatiq = False
131
+ self.huey = False
132
+ self.rq = False
133
+
134
+ @property
135
+ def any(self) -> bool:
136
+ return self.celery or self.dramatiq or self.huey or self.rq
137
+
138
+
139
+ def _detect_task_frameworks(parsed_file: ParsedFile) -> _DetectedFramework:
140
+ found = _DetectedFramework()
141
+ for imp in parsed_file.imports:
142
+ module = imp.module or ""
143
+ mod = module.split(".")[0]
144
+ if mod == "celery" or module.endswith(".celery"):
145
+ # Catches direct `from celery import ...` AND the common Django
146
+ # convention of `from myapp.celery import app` where the app's
147
+ # Celery instance lives in a local `celery.py` module.
148
+ found.celery = True
149
+ elif mod == "dramatiq":
150
+ found.dramatiq = True
151
+ elif mod == "huey":
152
+ found.huey = True
153
+ elif mod == "rq":
154
+ found.rq = True
155
+ for name in imp.names:
156
+ if name == "shared_task":
157
+ found.celery = True
158
+ elif name == "job" and imp.module and "rq" in imp.module:
159
+ found.rq = True
160
+ return found
161
+
162
+
163
+ # ── Plugin ──────────────────────────────────────────────────────────────────
164
+
165
+
166
+ class CeleryPlugin(BaseFrameworkPlugin):
167
+ """Plugin for Celery, Dramatiq, Huey, and RQ task frameworks."""
168
+
169
+ FRAMEWORK: ClassVar[Framework] = Framework.CELERY
170
+ LANGUAGE: ClassVar[Language] = Language.PYTHON
171
+ DETECTION_IMPORTS: ClassVar[frozenset[str]] = ALL_TASK_IMPORTS
172
+
173
+ # ------------------------------------------------------------------
174
+ # Detection
175
+ # ------------------------------------------------------------------
176
+
177
+ def detect(self, parsed_file: ParsedFile) -> bool:
178
+ return _detect_task_frameworks(parsed_file).any
179
+
180
+ # ------------------------------------------------------------------
181
+ # Route (task) extraction
182
+ # ------------------------------------------------------------------
183
+
184
+ def extract_routes(
185
+ self,
186
+ parsed_file: ParsedFile,
187
+ context: Any = None,
188
+ ) -> list[ExtractedRoute]:
189
+ fw = _detect_task_frameworks(parsed_file)
190
+ routes: list[ExtractedRoute] = []
191
+
192
+ for func in parsed_file.functions:
193
+ route = self._try_extract_task(func, fw)
194
+ if route:
195
+ routes.append(route)
196
+
197
+ for cls in parsed_file.classes:
198
+ for method in cls.methods:
199
+ route = self._try_extract_task(method, fw)
200
+ if route:
201
+ routes.append(route)
202
+
203
+ return routes
204
+
205
+ # ------------------------------------------------------------------
206
+ # Stubs — task frameworks don't define HTTP auth/deps/middleware
207
+ # ------------------------------------------------------------------
208
+
209
+ def extract_dependencies(self, parsed_file: ParsedFile) -> list[ExtractedDependency]:
210
+ return []
211
+
212
+ def extract_auth_schemes(self, parsed_file: ParsedFile) -> list[ExtractedAuthScheme]:
213
+ return []
214
+
215
+ def extract_auth_dependencies(
216
+ self,
217
+ parsed_file: ParsedFile,
218
+ known_scheme_names: set[str] | None = None,
219
+ **kwargs: Any,
220
+ ) -> list[ExtractedAuthDependency]:
221
+ return []
222
+
223
+ def extract_middleware(self, parsed_file: ParsedFile) -> list[ExtractedMiddleware]:
224
+ return []
225
+
226
+ # ------------------------------------------------------------------
227
+ # Internal
228
+ # ------------------------------------------------------------------
229
+
230
+ def _try_extract_task(
231
+ self,
232
+ func: ParsedFunction,
233
+ fw: _DetectedFramework,
234
+ ) -> ExtractedRoute | None:
235
+ """If *func* has a task decorator, return an ExtractedRoute with kind="task"."""
236
+ dec, origin = self._find_task_decorator(func, fw)
237
+ if dec is None:
238
+ return None
239
+
240
+ task_name = self._resolve_task_name(dec, func, origin)
241
+ params = self._extract_parameters(func, origin)
242
+ kind = "scheduled" if self._is_periodic(dec) else "task"
243
+
244
+ return ExtractedRoute(
245
+ kind=kind,
246
+ method=HttpMethod.POST,
247
+ path=task_name,
248
+ handler_function=func.qualified_name,
249
+ handler_location=func.location,
250
+ path_params=[],
251
+ query_params=params,
252
+ header_params=[],
253
+ cookie_params=[],
254
+ body=None,
255
+ response=ExtractedResponse(),
256
+ confidence=Confidence.HIGH,
257
+ )
258
+
259
+ def _find_task_decorator(
260
+ self,
261
+ func: ParsedFunction,
262
+ fw: _DetectedFramework,
263
+ ) -> tuple[ParsedDecorator | None, str]:
264
+ """Return (decorator, origin_framework) if a task decorator is found."""
265
+ for dec in func.decorators:
266
+ base = self._decorator_base_name(dec)
267
+
268
+ if fw.celery and base in CELERY_TASK_DECORATORS:
269
+ return dec, "celery"
270
+ if fw.dramatiq and base in DRAMATIQ_ACTOR_DECORATORS:
271
+ return dec, "dramatiq"
272
+ if fw.huey and base in HUEY_TASK_DECORATORS:
273
+ return dec, "huey"
274
+ if fw.rq and base in RQ_JOB_DECORATORS:
275
+ return dec, "rq"
276
+
277
+ return None, ""
278
+
279
+ def _resolve_task_name(
280
+ self,
281
+ dec: ParsedDecorator,
282
+ func: ParsedFunction,
283
+ origin: str,
284
+ ) -> str:
285
+ """Derive the canonical task name.
286
+
287
+ Priority: explicit ``name=`` kwarg > first positional string arg >
288
+ function qualified name.
289
+ """
290
+ name_kwarg_key = "actor_name" if origin == "dramatiq" else "name"
291
+ explicit = self._extract_decorator_arg(dec, name_kwarg_key)
292
+ if explicit and isinstance(explicit, str):
293
+ return explicit
294
+
295
+ if dec.positional_args:
296
+ first = dec.positional_args[0]
297
+ if isinstance(first, str):
298
+ return first
299
+
300
+ return func.qualified_name.full
301
+
302
+ @staticmethod
303
+ def _is_periodic(dec: ParsedDecorator) -> bool:
304
+ """True if the decorator indicates a periodic/scheduled task (Huey)."""
305
+ name = dec.name or ""
306
+ return "periodic" in name.lower()
307
+
308
+ def _extract_parameters(
309
+ self,
310
+ func: ParsedFunction,
311
+ origin: str,
312
+ ) -> list[ExtractedParameter]:
313
+ """Extract task argument parameters from the function signature.
314
+
315
+ Celery ``bind=True`` injects ``self`` as the first parameter — skip it.
316
+ """
317
+ params: list[ExtractedParameter] = []
318
+ skip_first = False
319
+
320
+ if origin == "celery":
321
+ for dec in func.decorators:
322
+ bind_val = self._extract_decorator_arg(dec, "bind")
323
+ if bind_val in (True, "True", "true"):
324
+ skip_first = True
325
+ break
326
+
327
+ skipped = False
328
+ for p in func.parameters:
329
+ if p.name in ("self", "cls"):
330
+ continue
331
+ if skip_first and not skipped:
332
+ skipped = True
333
+ continue
334
+ if p.is_variadic or p.is_keyword_variadic:
335
+ continue
336
+
337
+ params.append(
338
+ ExtractedParameter(
339
+ name=p.name,
340
+ location=ParameterLocation.TASK_ARGUMENT,
341
+ type_annotation=p.type_annotation,
342
+ required=p.default_value is None,
343
+ default_value=p.default_value,
344
+ )
345
+ )
346
+
347
+ return params
348
+
349
+ @staticmethod
350
+ def _decorator_base_name(dec: ParsedDecorator) -> str:
351
+ """Normalise a decorator to a lookup key.
352
+
353
+ Handles:
354
+ - ``celery.shared_task`` (qualified)
355
+ - ``shared_task`` (bare import)
356
+ - ``app.task`` (attribute on Celery instance)
357
+ - ``dramatiq.actor`` (qualified)
358
+ - ``huey.task`` / ``huey.periodic_task`` (attribute on Huey instance)
359
+ - ``job`` (bare RQ import)
360
+ """
361
+ if dec.qualified_name:
362
+ qn = dec.qualified_name.full
363
+ parts = qn.rsplit(".", 1)
364
+ if len(parts) == 2:
365
+ mod, attr = parts
366
+ if mod in ("celery", "dramatiq", "rq.decorators"):
367
+ return f"{mod}.{attr}"
368
+ if attr in (
369
+ "task",
370
+ "shared_task",
371
+ "actor",
372
+ "periodic_task",
373
+ "job",
374
+ ):
375
+ return attr
376
+ return qn
377
+
378
+ name = dec.name
379
+ if "." in name:
380
+ _obj, _, attr = name.rpartition(".")
381
+ if attr in (
382
+ "task",
383
+ "shared_task",
384
+ "actor",
385
+ "periodic_task",
386
+ "job",
387
+ ):
388
+ return attr
389
+ return name
390
+
391
+
392
+ _celery_plugin = CeleryPlugin()
393
+ FrameworkPluginRegistry.register(_celery_plugin)