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,251 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prefect workflow framework plugin.
|
|
3
|
+
|
|
4
|
+
Detects flow and task entry points from Prefect 3.x decorator patterns.
|
|
5
|
+
|
|
6
|
+
Supported:
|
|
7
|
+
- @flow / @flow(name="...") — Prefect flow definitions
|
|
8
|
+
- @task / @task(name="...") — Prefect task definitions
|
|
9
|
+
- @prefect.flow / @prefect.task — qualified forms
|
|
10
|
+
- Function parameter extraction as task arguments
|
|
11
|
+
- Type annotation preservation (used by Prefect for Pydantic validation)
|
|
12
|
+
|
|
13
|
+
Not supported (yet):
|
|
14
|
+
- serve() / deploy() lifecycle methods
|
|
15
|
+
- Prefect blocks (Secret, etc.) as data sources
|
|
16
|
+
- Subflow detection (flow calling flow)
|
|
17
|
+
- Task result caching semantics
|
|
18
|
+
- Prefect events and automations
|
|
19
|
+
- Work pool / worker configuration analysis
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Any, ClassVar
|
|
25
|
+
|
|
26
|
+
from ...core.types import (
|
|
27
|
+
Confidence,
|
|
28
|
+
Framework,
|
|
29
|
+
HttpMethod,
|
|
30
|
+
Language,
|
|
31
|
+
ParameterLocation,
|
|
32
|
+
)
|
|
33
|
+
from ...parsing.base import ParsedDecorator, ParsedFile, ParsedFunction
|
|
34
|
+
from ..base import (
|
|
35
|
+
BaseFrameworkPlugin,
|
|
36
|
+
ExtractedAuthDependency,
|
|
37
|
+
ExtractedAuthScheme,
|
|
38
|
+
ExtractedDependency,
|
|
39
|
+
ExtractedMiddleware,
|
|
40
|
+
ExtractedParameter,
|
|
41
|
+
ExtractedResponse,
|
|
42
|
+
ExtractedRoute,
|
|
43
|
+
FrameworkPluginRegistry,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# ── Import detection ────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
PREFECT_IMPORTS = frozenset({"prefect"})
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ── Decorator name sets ─────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
FLOW_DECORATORS: set[str] = {
|
|
54
|
+
"flow", # @flow (bare import)
|
|
55
|
+
"prefect.flow", # @prefect.flow (qualified)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
TASK_DECORATORS: set[str] = {
|
|
59
|
+
"task", # @task (bare import)
|
|
60
|
+
"prefect.task", # @prefect.task (qualified)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
ALL_PREFECT_DECORATORS = FLOW_DECORATORS | TASK_DECORATORS
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ── Plugin ──────────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class PrefectPlugin(BaseFrameworkPlugin):
|
|
70
|
+
"""Plugin for Prefect 3.x flow and task framework extraction."""
|
|
71
|
+
|
|
72
|
+
FRAMEWORK: ClassVar[Framework] = Framework.PREFECT
|
|
73
|
+
LANGUAGE: ClassVar[Language] = Language.PYTHON
|
|
74
|
+
DETECTION_IMPORTS: ClassVar[frozenset[str]] = PREFECT_IMPORTS
|
|
75
|
+
|
|
76
|
+
# ------------------------------------------------------------------
|
|
77
|
+
# Detection
|
|
78
|
+
# ------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
def detect(self, parsed_file: ParsedFile) -> bool:
|
|
81
|
+
for imp in parsed_file.imports:
|
|
82
|
+
mod = (imp.module or "").split(".")[0]
|
|
83
|
+
if mod == "prefect":
|
|
84
|
+
return True
|
|
85
|
+
for name in imp.names:
|
|
86
|
+
if name in ("prefect", "flow", "task") and (
|
|
87
|
+
imp.module is None or imp.module.split(".")[0] == "prefect"
|
|
88
|
+
):
|
|
89
|
+
return True
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
# ------------------------------------------------------------------
|
|
93
|
+
# Route (flow/task) extraction
|
|
94
|
+
# ------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
def extract_routes(
|
|
97
|
+
self,
|
|
98
|
+
parsed_file: ParsedFile,
|
|
99
|
+
context: Any = None,
|
|
100
|
+
) -> list[ExtractedRoute]:
|
|
101
|
+
routes: list[ExtractedRoute] = []
|
|
102
|
+
|
|
103
|
+
for func in parsed_file.functions:
|
|
104
|
+
route = self._try_extract_entry_point(func)
|
|
105
|
+
if route:
|
|
106
|
+
routes.append(route)
|
|
107
|
+
|
|
108
|
+
for cls in parsed_file.classes:
|
|
109
|
+
for method in cls.methods:
|
|
110
|
+
route = self._try_extract_entry_point(method)
|
|
111
|
+
if route:
|
|
112
|
+
routes.append(route)
|
|
113
|
+
|
|
114
|
+
return routes
|
|
115
|
+
|
|
116
|
+
# ------------------------------------------------------------------
|
|
117
|
+
# Stubs — Prefect has no HTTP auth/deps/middleware
|
|
118
|
+
# ------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
def extract_dependencies(self, parsed_file: ParsedFile) -> list[ExtractedDependency]:
|
|
121
|
+
return []
|
|
122
|
+
|
|
123
|
+
def extract_auth_schemes(self, parsed_file: ParsedFile) -> list[ExtractedAuthScheme]:
|
|
124
|
+
return []
|
|
125
|
+
|
|
126
|
+
def extract_auth_dependencies(
|
|
127
|
+
self,
|
|
128
|
+
parsed_file: ParsedFile,
|
|
129
|
+
known_scheme_names: set[str] | None = None,
|
|
130
|
+
**kwargs: Any,
|
|
131
|
+
) -> list[ExtractedAuthDependency]:
|
|
132
|
+
return []
|
|
133
|
+
|
|
134
|
+
def extract_middleware(self, parsed_file: ParsedFile) -> list[ExtractedMiddleware]:
|
|
135
|
+
return []
|
|
136
|
+
|
|
137
|
+
# ------------------------------------------------------------------
|
|
138
|
+
# Internal
|
|
139
|
+
# ------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
def _try_extract_entry_point(
|
|
142
|
+
self,
|
|
143
|
+
func: ParsedFunction,
|
|
144
|
+
) -> ExtractedRoute | None:
|
|
145
|
+
"""If *func* has a @flow or @task decorator, return an ExtractedRoute."""
|
|
146
|
+
dec, is_flow = self._find_prefect_decorator(func)
|
|
147
|
+
if dec is None:
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
entry_name = self._resolve_name(dec, func)
|
|
151
|
+
params = self._extract_parameters(func)
|
|
152
|
+
|
|
153
|
+
return ExtractedRoute(
|
|
154
|
+
kind="task",
|
|
155
|
+
method=HttpMethod.POST,
|
|
156
|
+
path=entry_name,
|
|
157
|
+
handler_function=func.qualified_name,
|
|
158
|
+
handler_location=func.location,
|
|
159
|
+
path_params=[],
|
|
160
|
+
query_params=params,
|
|
161
|
+
header_params=[],
|
|
162
|
+
cookie_params=[],
|
|
163
|
+
body=None,
|
|
164
|
+
response=ExtractedResponse(),
|
|
165
|
+
confidence=Confidence.HIGH,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def _find_prefect_decorator(
|
|
169
|
+
self,
|
|
170
|
+
func: ParsedFunction,
|
|
171
|
+
) -> tuple[ParsedDecorator | None, bool]:
|
|
172
|
+
"""Return (decorator, is_flow) if a Prefect decorator is found.
|
|
173
|
+
|
|
174
|
+
``is_flow`` is True for @flow, False for @task.
|
|
175
|
+
"""
|
|
176
|
+
for dec in func.decorators:
|
|
177
|
+
base = self._decorator_base_name(dec)
|
|
178
|
+
if base in FLOW_DECORATORS:
|
|
179
|
+
return dec, True
|
|
180
|
+
if base in TASK_DECORATORS:
|
|
181
|
+
return dec, False
|
|
182
|
+
return None, False
|
|
183
|
+
|
|
184
|
+
def _resolve_name(
|
|
185
|
+
self,
|
|
186
|
+
dec: ParsedDecorator,
|
|
187
|
+
func: ParsedFunction,
|
|
188
|
+
) -> str:
|
|
189
|
+
"""Derive the canonical flow/task name.
|
|
190
|
+
|
|
191
|
+
Priority: explicit ``name=`` kwarg > function name (no dash conversion).
|
|
192
|
+
"""
|
|
193
|
+
explicit = self._extract_decorator_arg(dec, "name")
|
|
194
|
+
if explicit and isinstance(explicit, str):
|
|
195
|
+
return explicit
|
|
196
|
+
return func.qualified_name.full
|
|
197
|
+
|
|
198
|
+
def _extract_parameters(
|
|
199
|
+
self,
|
|
200
|
+
func: ParsedFunction,
|
|
201
|
+
) -> list[ExtractedParameter]:
|
|
202
|
+
"""Extract task/flow argument parameters from the function signature."""
|
|
203
|
+
params: list[ExtractedParameter] = []
|
|
204
|
+
|
|
205
|
+
for p in func.parameters:
|
|
206
|
+
if p.name in ("self", "cls"):
|
|
207
|
+
continue
|
|
208
|
+
if p.is_variadic or p.is_keyword_variadic:
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
params.append(
|
|
212
|
+
ExtractedParameter(
|
|
213
|
+
name=p.name,
|
|
214
|
+
location=ParameterLocation.TASK_ARGUMENT,
|
|
215
|
+
type_annotation=p.type_annotation,
|
|
216
|
+
required=p.default_value is None,
|
|
217
|
+
default_value=p.default_value,
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
return params
|
|
222
|
+
|
|
223
|
+
@staticmethod
|
|
224
|
+
def _decorator_base_name(dec: ParsedDecorator) -> str:
|
|
225
|
+
"""Normalise a decorator to a lookup key.
|
|
226
|
+
|
|
227
|
+
Handles:
|
|
228
|
+
- ``prefect.flow`` / ``prefect.task`` (qualified)
|
|
229
|
+
- ``flow`` / ``task`` (bare import)
|
|
230
|
+
"""
|
|
231
|
+
if dec.qualified_name:
|
|
232
|
+
qn = dec.qualified_name.full
|
|
233
|
+
parts = qn.rsplit(".", 1)
|
|
234
|
+
if len(parts) == 2:
|
|
235
|
+
mod, attr = parts
|
|
236
|
+
if mod == "prefect":
|
|
237
|
+
return f"prefect.{attr}"
|
|
238
|
+
if attr in ("flow", "task"):
|
|
239
|
+
return attr
|
|
240
|
+
return qn
|
|
241
|
+
|
|
242
|
+
name = dec.name
|
|
243
|
+
if "." in name:
|
|
244
|
+
_obj, _, attr = name.rpartition(".")
|
|
245
|
+
if attr in ("flow", "task"):
|
|
246
|
+
return attr
|
|
247
|
+
return name
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
_prefect_plugin = PrefectPlugin()
|
|
251
|
+
FrameworkPluginRegistry.register(_prefect_plugin)
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generic webhook and event handler plugin.
|
|
3
|
+
|
|
4
|
+
Detects event-driven entry points across multiple frameworks and patterns.
|
|
5
|
+
|
|
6
|
+
Supported constructs:
|
|
7
|
+
- FastAPI/Starlette @app.on_event("startup") / @app.on_event("shutdown")
|
|
8
|
+
- Django signals: @receiver(post_save, ...) — from django.dispatch
|
|
9
|
+
- Generic webhook handlers: @app.webhook, @webhook_handler
|
|
10
|
+
- Flask-SocketIO @socketio.on("event_name")
|
|
11
|
+
- gidgethub Router: @router.register("event_type", action="...") — GitHub webhook bots
|
|
12
|
+
- asyncio event loop callbacks (limited)
|
|
13
|
+
|
|
14
|
+
Not supported (yet):
|
|
15
|
+
- Django Channels consumers
|
|
16
|
+
- AWS Lambda handler convention (def handler(event, context))
|
|
17
|
+
- Cloud Functions / Azure Functions triggers
|
|
18
|
+
- Pub/Sub subscription callbacks
|
|
19
|
+
- Custom event bus implementations
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Any, ClassVar
|
|
25
|
+
|
|
26
|
+
from ...core.types import (
|
|
27
|
+
Confidence,
|
|
28
|
+
Framework,
|
|
29
|
+
HttpMethod,
|
|
30
|
+
Language,
|
|
31
|
+
ParameterLocation,
|
|
32
|
+
)
|
|
33
|
+
from ...parsing.base import ParsedDecorator, ParsedFile, ParsedFunction
|
|
34
|
+
from ..base import (
|
|
35
|
+
BaseFrameworkPlugin,
|
|
36
|
+
ExtractedAuthDependency,
|
|
37
|
+
ExtractedAuthScheme,
|
|
38
|
+
ExtractedDependency,
|
|
39
|
+
ExtractedMiddleware,
|
|
40
|
+
ExtractedParameter,
|
|
41
|
+
ExtractedResponse,
|
|
42
|
+
ExtractedRoute,
|
|
43
|
+
FrameworkPluginRegistry,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# ── Decorator sets ──────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
EVENT_DECORATORS: set[str] = {
|
|
49
|
+
"on_event", # @app.on_event("startup")
|
|
50
|
+
"on", # @socketio.on("message")
|
|
51
|
+
"receiver", # Django signal: @receiver(post_save, ...)
|
|
52
|
+
"on_event_handler",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
WEBHOOK_DECORATORS: set[str] = {
|
|
56
|
+
"webhook", # @app.webhook(...)
|
|
57
|
+
"webhook_handler",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
ALL_EVENT_DECORATORS = EVENT_DECORATORS | WEBHOOK_DECORATORS
|
|
61
|
+
|
|
62
|
+
# FastAPI/Starlette lifecycle event names. These are not HTTP endpoints: they
|
|
63
|
+
# run only during application startup/shutdown and cannot receive external
|
|
64
|
+
# requests. They should be classified as kind="lifecycle" so downstream rules
|
|
65
|
+
# (missing_auth, rate_limiting, etc.) can skip them.
|
|
66
|
+
LIFECYCLE_EVENT_NAMES: set[str] = {
|
|
67
|
+
"startup",
|
|
68
|
+
"shutdown",
|
|
69
|
+
"lifespan",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class WebhookEventPlugin(BaseFrameworkPlugin):
|
|
74
|
+
"""Generic plugin for webhook and event handler entry points."""
|
|
75
|
+
|
|
76
|
+
FRAMEWORK: ClassVar[Framework] = Framework.GENERIC
|
|
77
|
+
LANGUAGE: ClassVar[Language] = Language.PYTHON
|
|
78
|
+
DETECTION_IMPORTS: ClassVar[frozenset[str]] = frozenset(
|
|
79
|
+
{
|
|
80
|
+
"django.dispatch",
|
|
81
|
+
"flask_socketio",
|
|
82
|
+
"gidgethub",
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def detect(self, parsed_file: ParsedFile) -> bool:
|
|
87
|
+
is_gidgethub = self._has_gidgethub_import(parsed_file)
|
|
88
|
+
for func in parsed_file.functions:
|
|
89
|
+
for dec in func.decorators:
|
|
90
|
+
base = self._decorator_base_name(dec)
|
|
91
|
+
if base in ALL_EVENT_DECORATORS:
|
|
92
|
+
return True
|
|
93
|
+
if is_gidgethub and base == "register":
|
|
94
|
+
return True
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
def extract_routes(
|
|
98
|
+
self,
|
|
99
|
+
parsed_file: ParsedFile,
|
|
100
|
+
context: Any = None,
|
|
101
|
+
) -> list[ExtractedRoute]:
|
|
102
|
+
routes: list[ExtractedRoute] = []
|
|
103
|
+
is_gidgethub = self._has_gidgethub_import(parsed_file)
|
|
104
|
+
|
|
105
|
+
for func in parsed_file.functions:
|
|
106
|
+
route = self._try_extract_event(func, is_gidgethub=is_gidgethub)
|
|
107
|
+
if route:
|
|
108
|
+
routes.append(route)
|
|
109
|
+
|
|
110
|
+
for cls in parsed_file.classes:
|
|
111
|
+
for method in cls.methods:
|
|
112
|
+
route = self._try_extract_event(method, is_gidgethub=is_gidgethub)
|
|
113
|
+
if route:
|
|
114
|
+
routes.append(route)
|
|
115
|
+
|
|
116
|
+
return routes
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def _has_gidgethub_import(parsed_file: ParsedFile) -> bool:
|
|
120
|
+
return any((imp.module or "").startswith("gidgethub") for imp in parsed_file.imports)
|
|
121
|
+
|
|
122
|
+
def extract_dependencies(self, parsed_file: ParsedFile) -> list[ExtractedDependency]:
|
|
123
|
+
return []
|
|
124
|
+
|
|
125
|
+
def extract_auth_schemes(self, parsed_file: ParsedFile) -> list[ExtractedAuthScheme]:
|
|
126
|
+
return []
|
|
127
|
+
|
|
128
|
+
def extract_auth_dependencies(
|
|
129
|
+
self,
|
|
130
|
+
parsed_file: ParsedFile,
|
|
131
|
+
known_scheme_names: set[str] | None = None,
|
|
132
|
+
**kwargs: Any,
|
|
133
|
+
) -> list[ExtractedAuthDependency]:
|
|
134
|
+
return []
|
|
135
|
+
|
|
136
|
+
def extract_middleware(self, parsed_file: ParsedFile) -> list[ExtractedMiddleware]:
|
|
137
|
+
return []
|
|
138
|
+
|
|
139
|
+
def _try_extract_event(
|
|
140
|
+
self, func: ParsedFunction, *, is_gidgethub: bool = False
|
|
141
|
+
) -> ExtractedRoute | None:
|
|
142
|
+
dec = self._find_event_decorator(func, is_gidgethub=is_gidgethub)
|
|
143
|
+
if dec is None:
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
base = self._decorator_base_name(dec)
|
|
147
|
+
is_webhook = base in WEBHOOK_DECORATORS
|
|
148
|
+
is_gh_register = is_gidgethub and base == "register"
|
|
149
|
+
event_name = self._resolve_event_name(dec, func, is_gh_register=is_gh_register)
|
|
150
|
+
params = self._extract_parameters(func)
|
|
151
|
+
|
|
152
|
+
if is_webhook or is_gh_register:
|
|
153
|
+
kind = "webhook"
|
|
154
|
+
elif base == "on_event" and event_name in LIFECYCLE_EVENT_NAMES:
|
|
155
|
+
# FastAPI @app.on_event("startup"|"shutdown") — not an HTTP endpoint.
|
|
156
|
+
# The engine's missing_auth / rate_limiting rules must skip entry
|
|
157
|
+
# points with kind="lifecycle" since they cannot receive external
|
|
158
|
+
# requests.
|
|
159
|
+
kind = "lifecycle"
|
|
160
|
+
else:
|
|
161
|
+
kind = "event"
|
|
162
|
+
|
|
163
|
+
return ExtractedRoute(
|
|
164
|
+
kind=kind,
|
|
165
|
+
method=HttpMethod.POST,
|
|
166
|
+
path=event_name,
|
|
167
|
+
handler_function=func.qualified_name,
|
|
168
|
+
handler_location=func.location,
|
|
169
|
+
path_params=[],
|
|
170
|
+
query_params=params,
|
|
171
|
+
header_params=[],
|
|
172
|
+
cookie_params=[],
|
|
173
|
+
body=None,
|
|
174
|
+
response=ExtractedResponse(),
|
|
175
|
+
confidence=Confidence.MEDIUM,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def _find_event_decorator(
|
|
179
|
+
self, func: ParsedFunction, *, is_gidgethub: bool = False
|
|
180
|
+
) -> ParsedDecorator | None:
|
|
181
|
+
for dec in func.decorators:
|
|
182
|
+
base = self._decorator_base_name(dec)
|
|
183
|
+
if base in ALL_EVENT_DECORATORS:
|
|
184
|
+
return dec
|
|
185
|
+
if is_gidgethub and base == "register":
|
|
186
|
+
return dec
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
def _resolve_event_name(
|
|
190
|
+
self,
|
|
191
|
+
dec: ParsedDecorator,
|
|
192
|
+
func: ParsedFunction,
|
|
193
|
+
*,
|
|
194
|
+
is_gh_register: bool = False,
|
|
195
|
+
) -> str:
|
|
196
|
+
if dec.positional_args:
|
|
197
|
+
first = dec.positional_args[0]
|
|
198
|
+
if isinstance(first, str):
|
|
199
|
+
event = first
|
|
200
|
+
if is_gh_register:
|
|
201
|
+
# @router.register("pull_request", action="opened") →
|
|
202
|
+
# emit "pull_request:opened" so distinct actions are separate routes.
|
|
203
|
+
action = dec.arguments.get("action")
|
|
204
|
+
if action and isinstance(action, str):
|
|
205
|
+
event = f"{event}:{action}"
|
|
206
|
+
return event
|
|
207
|
+
explicit = self._extract_decorator_arg(dec, "name")
|
|
208
|
+
if explicit and isinstance(explicit, str):
|
|
209
|
+
return explicit
|
|
210
|
+
signal_arg = self._extract_decorator_arg(dec, "signal")
|
|
211
|
+
if signal_arg and isinstance(signal_arg, str):
|
|
212
|
+
return signal_arg
|
|
213
|
+
return func.name
|
|
214
|
+
|
|
215
|
+
def _extract_parameters(self, func: ParsedFunction) -> list[ExtractedParameter]:
|
|
216
|
+
params: list[ExtractedParameter] = []
|
|
217
|
+
for p in func.parameters:
|
|
218
|
+
if p.name in ("self", "cls", "sender", "signal", "**kwargs"):
|
|
219
|
+
continue
|
|
220
|
+
if p.is_variadic or p.is_keyword_variadic:
|
|
221
|
+
continue
|
|
222
|
+
params.append(
|
|
223
|
+
ExtractedParameter(
|
|
224
|
+
name=p.name,
|
|
225
|
+
location=ParameterLocation.TASK_ARGUMENT,
|
|
226
|
+
type_annotation=p.type_annotation,
|
|
227
|
+
required=p.default_value is None,
|
|
228
|
+
default_value=p.default_value,
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
return params
|
|
232
|
+
|
|
233
|
+
@staticmethod
|
|
234
|
+
def _decorator_base_name(dec: ParsedDecorator) -> str:
|
|
235
|
+
if dec.qualified_name:
|
|
236
|
+
qn = dec.qualified_name.full
|
|
237
|
+
parts = qn.rsplit(".", 1)
|
|
238
|
+
if len(parts) == 2:
|
|
239
|
+
_, attr = parts
|
|
240
|
+
# Return the attribute name for known decorators and for
|
|
241
|
+
# `register` (gidgethub Router pattern).
|
|
242
|
+
if attr in ALL_EVENT_DECORATORS or attr == "register":
|
|
243
|
+
return attr
|
|
244
|
+
return qn
|
|
245
|
+
|
|
246
|
+
name = dec.name
|
|
247
|
+
if "." in name:
|
|
248
|
+
_obj, _, attr = name.rpartition(".")
|
|
249
|
+
if attr in ALL_EVENT_DECORATORS or attr == "register":
|
|
250
|
+
return attr
|
|
251
|
+
return name
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
_webhook_plugin = WebhookEventPlugin()
|
|
255
|
+
FrameworkPluginRegistry.register(_webhook_plugin)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Language-specific parsing modules."""
|
|
2
|
+
|
|
3
|
+
from .base import (
|
|
4
|
+
BaseParser,
|
|
5
|
+
ParsedArgument,
|
|
6
|
+
ParsedAssignment,
|
|
7
|
+
ParsedCallSite,
|
|
8
|
+
ParsedClass,
|
|
9
|
+
ParsedDecorator,
|
|
10
|
+
ParsedFile,
|
|
11
|
+
ParsedFunction,
|
|
12
|
+
ParsedImport,
|
|
13
|
+
ParsedParameter,
|
|
14
|
+
ParsedSymbol,
|
|
15
|
+
ParserRegistry,
|
|
16
|
+
)
|
|
17
|
+
from .services import (
|
|
18
|
+
# Data types
|
|
19
|
+
AnalysisContext,
|
|
20
|
+
ConstantResolver,
|
|
21
|
+
# Abstract base
|
|
22
|
+
LanguageServices,
|
|
23
|
+
PathResolver,
|
|
24
|
+
ResolvedConstant,
|
|
25
|
+
ResolvedField,
|
|
26
|
+
ResolvedPath,
|
|
27
|
+
ResolvedType,
|
|
28
|
+
RouterInfo,
|
|
29
|
+
RouterRegistry,
|
|
30
|
+
# Protocols
|
|
31
|
+
TypeResolver,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
# Base parsing
|
|
36
|
+
"BaseParser",
|
|
37
|
+
"ParserRegistry",
|
|
38
|
+
"ParsedFile",
|
|
39
|
+
"ParsedFunction",
|
|
40
|
+
"ParsedClass",
|
|
41
|
+
"ParsedImport",
|
|
42
|
+
"ParsedCallSite",
|
|
43
|
+
"ParsedArgument",
|
|
44
|
+
"ParsedAssignment",
|
|
45
|
+
"ParsedSymbol",
|
|
46
|
+
"ParsedParameter",
|
|
47
|
+
"ParsedDecorator",
|
|
48
|
+
# Service protocols
|
|
49
|
+
"TypeResolver",
|
|
50
|
+
"ConstantResolver",
|
|
51
|
+
"PathResolver",
|
|
52
|
+
"RouterRegistry",
|
|
53
|
+
# Service data types
|
|
54
|
+
"AnalysisContext",
|
|
55
|
+
"ResolvedType",
|
|
56
|
+
"ResolvedField",
|
|
57
|
+
"ResolvedConstant",
|
|
58
|
+
"ResolvedPath",
|
|
59
|
+
"RouterInfo",
|
|
60
|
+
# Language services
|
|
61
|
+
"LanguageServices",
|
|
62
|
+
]
|