entrygraph 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.
- entrygraph/__init__.py +87 -0
- entrygraph/__main__.py +8 -0
- entrygraph/_version.py +24 -0
- entrygraph/api.py +549 -0
- entrygraph/cli/__init__.py +0 -0
- entrygraph/cli/main.py +387 -0
- entrygraph/cli/render.py +136 -0
- entrygraph/data/sinks/csharp.toml +106 -0
- entrygraph/data/sinks/go.toml +87 -0
- entrygraph/data/sinks/java.toml +92 -0
- entrygraph/data/sinks/javascript.toml +112 -0
- entrygraph/data/sinks/lib_javascript.toml +34 -0
- entrygraph/data/sinks/lib_python.toml +39 -0
- entrygraph/data/sinks/php.toml +125 -0
- entrygraph/data/sinks/python.toml +160 -0
- entrygraph/data/sinks/ruby.toml +102 -0
- entrygraph/data/sinks/rust.toml +68 -0
- entrygraph/db/__init__.py +0 -0
- entrygraph/db/engine.py +34 -0
- entrygraph/db/meta.py +70 -0
- entrygraph/db/models.py +176 -0
- entrygraph/db/queries.py +155 -0
- entrygraph/detect/__init__.py +0 -0
- entrygraph/detect/entrypoints/__init__.py +22 -0
- entrygraph/detect/entrypoints/base.py +124 -0
- entrygraph/detect/entrypoints/configs.py +139 -0
- entrygraph/detect/entrypoints/csharp.py +156 -0
- entrygraph/detect/entrypoints/golang.py +158 -0
- entrygraph/detect/entrypoints/java.py +187 -0
- entrygraph/detect/entrypoints/javascript.py +211 -0
- entrygraph/detect/entrypoints/php.py +133 -0
- entrygraph/detect/entrypoints/python.py +335 -0
- entrygraph/detect/entrypoints/ruby.py +147 -0
- entrygraph/detect/entrypoints/rust.py +153 -0
- entrygraph/detect/frameworks.py +369 -0
- entrygraph/detect/manifests.py +234 -0
- entrygraph/detect/taint.py +224 -0
- entrygraph/errors.py +27 -0
- entrygraph/extract/__init__.py +0 -0
- entrygraph/extract/base.py +51 -0
- entrygraph/extract/csharp.py +502 -0
- entrygraph/extract/golang.py +342 -0
- entrygraph/extract/ir.py +105 -0
- entrygraph/extract/java.py +329 -0
- entrygraph/extract/javascript.py +400 -0
- entrygraph/extract/php.py +426 -0
- entrygraph/extract/python.py +390 -0
- entrygraph/extract/registry.py +43 -0
- entrygraph/extract/ruby.py +321 -0
- entrygraph/extract/rust.py +482 -0
- entrygraph/fs/__init__.py +0 -0
- entrygraph/fs/hashing.py +78 -0
- entrygraph/fs/lang.py +134 -0
- entrygraph/fs/walker.py +167 -0
- entrygraph/graph/__init__.py +0 -0
- entrygraph/graph/adjacency.py +146 -0
- entrygraph/graph/cte.py +123 -0
- entrygraph/graph/scoring.py +101 -0
- entrygraph/kinds.py +51 -0
- entrygraph/parsing/__init__.py +0 -0
- entrygraph/parsing/parsers.py +49 -0
- entrygraph/parsing/queries.py +39 -0
- entrygraph/pipeline/__init__.py +0 -0
- entrygraph/pipeline/scanner.py +506 -0
- entrygraph/pipeline/worker.py +49 -0
- entrygraph/pipeline/writer.py +41 -0
- entrygraph/py.typed +0 -0
- entrygraph/queries/csharp/calls.scm +4 -0
- entrygraph/queries/csharp/definitions.scm +29 -0
- entrygraph/queries/csharp/imports.scm +4 -0
- entrygraph/queries/go/calls.scm +2 -0
- entrygraph/queries/go/definitions.scm +24 -0
- entrygraph/queries/go/imports.scm +1 -0
- entrygraph/queries/java/calls.scm +2 -0
- entrygraph/queries/java/definitions.scm +14 -0
- entrygraph/queries/java/imports.scm +2 -0
- entrygraph/queries/javascript/calls.scm +4 -0
- entrygraph/queries/javascript/definitions.scm +4 -0
- entrygraph/queries/javascript/imports.scm +6 -0
- entrygraph/queries/php/calls.scm +8 -0
- entrygraph/queries/php/definitions.scm +24 -0
- entrygraph/queries/php/imports.scm +1 -0
- entrygraph/queries/python/calls.scm +2 -0
- entrygraph/queries/python/definitions.scm +11 -0
- entrygraph/queries/python/imports.scm +2 -0
- entrygraph/queries/ruby/calls.scm +4 -0
- entrygraph/queries/ruby/definitions.scm +20 -0
- entrygraph/queries/ruby/imports.scm +7 -0
- entrygraph/queries/rust/calls.scm +5 -0
- entrygraph/queries/rust/definitions.scm +26 -0
- entrygraph/queries/rust/imports.scm +4 -0
- entrygraph/resolve/__init__.py +0 -0
- entrygraph/resolve/externals.py +61 -0
- entrygraph/resolve/hierarchy.py +152 -0
- entrygraph/resolve/resolver.py +275 -0
- entrygraph/resolve/symbol_table.py +48 -0
- entrygraph/results.py +138 -0
- entrygraph-0.1.0.dist-info/METADATA +204 -0
- entrygraph-0.1.0.dist-info/RECORD +102 -0
- entrygraph-0.1.0.dist-info/WHEEL +4 -0
- entrygraph-0.1.0.dist-info/entry_points.txt +2 -0
- entrygraph-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""Python entrypoint rules: flask, fastapi, django urls, click/typer, celery,
|
|
2
|
+
lambda handlers, and the language-core __main__ guard."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from entrygraph.detect.entrypoints.base import (
|
|
9
|
+
EntrypointRule,
|
|
10
|
+
first_string_arg,
|
|
11
|
+
identifier_args,
|
|
12
|
+
methods_kwarg,
|
|
13
|
+
register,
|
|
14
|
+
tainted_params,
|
|
15
|
+
)
|
|
16
|
+
from entrygraph.extract.ir import EntrypointHint, FileExtraction, RawSymbol
|
|
17
|
+
from entrygraph.kinds import EntrypointKind, SymbolKind
|
|
18
|
+
|
|
19
|
+
_FLASK_ROUTE = re.compile(r"^@(\w+)\.route\(")
|
|
20
|
+
_HTTP_VERB = re.compile(r"^@(\w+)\.(get|post|put|delete|patch|head|options)\(")
|
|
21
|
+
_CLICK_CMD = re.compile(r"^@(?:click\.(command|group)|(\w+)\.(command|group))\b")
|
|
22
|
+
_CELERY_TASK = re.compile(r"^@(?:shared_task\b|task\b|(\w+)\.task\b)")
|
|
23
|
+
_MIDDLEWARE_DECORATOR = re.compile(
|
|
24
|
+
r"^@(\w+)\.(before_request|after_request|before_first_request|errorhandler|middleware|teardown_request)\b"
|
|
25
|
+
)
|
|
26
|
+
_DJANGO_PATH_CALL = frozenset({"path", "re_path", "url"})
|
|
27
|
+
_LAMBDA_NAMES = frozenset({"lambda_handler", "handler"})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _decorated(x: FileExtraction) -> list[tuple[RawSymbol, str]]:
|
|
31
|
+
return [(s, d) for s in x.symbols for d in s.decorators
|
|
32
|
+
if s.kind in (SymbolKind.FUNCTION, SymbolKind.METHOD)]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _taint_meta(symbol: RawSymbol, kind: str) -> dict:
|
|
36
|
+
params = tainted_params(symbol.signature, kind)
|
|
37
|
+
return {"tainted_params": params} if params else {}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _flask_routes(x: FileExtraction) -> list[EntrypointHint]:
|
|
41
|
+
hints = []
|
|
42
|
+
for symbol, decorator in _decorated(x):
|
|
43
|
+
route = None
|
|
44
|
+
methods: list[str] = []
|
|
45
|
+
if _FLASK_ROUTE.match(decorator):
|
|
46
|
+
route = first_string_arg(decorator)
|
|
47
|
+
methods = methods_kwarg(decorator) or ["GET"]
|
|
48
|
+
else:
|
|
49
|
+
verb = _HTTP_VERB.match(decorator)
|
|
50
|
+
if verb:
|
|
51
|
+
route = first_string_arg(decorator)
|
|
52
|
+
methods = [verb.group(2).upper()]
|
|
53
|
+
if route is not None:
|
|
54
|
+
hints.append(
|
|
55
|
+
EntrypointHint(
|
|
56
|
+
rule_id="python.flask.route",
|
|
57
|
+
kind=EntrypointKind.HTTP_ROUTE,
|
|
58
|
+
handler_qualified_name=symbol.qualified_name,
|
|
59
|
+
route=route,
|
|
60
|
+
http_methods=methods,
|
|
61
|
+
framework="flask",
|
|
62
|
+
metadata=_taint_meta(symbol, "http_route"),
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
return hints
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _flask_add_url_rule(x: FileExtraction) -> list[EntrypointHint]:
|
|
69
|
+
"""Call-based route registration: app.add_url_rule("/x", view_func=handler)."""
|
|
70
|
+
hints = []
|
|
71
|
+
for ref in x.references:
|
|
72
|
+
if ref.kind != "call" or ref.callee_name != "add_url_rule" or not ref.arg_preview:
|
|
73
|
+
continue
|
|
74
|
+
route = first_string_arg("(" + ref.arg_preview.lstrip("("))
|
|
75
|
+
handler = None
|
|
76
|
+
for name in identifier_args(ref.arg_preview):
|
|
77
|
+
candidate = f"{x.module_path}.{name}"
|
|
78
|
+
if any(s.qualified_name == candidate for s in x.symbols):
|
|
79
|
+
handler = candidate
|
|
80
|
+
break
|
|
81
|
+
hints.append(
|
|
82
|
+
EntrypointHint(
|
|
83
|
+
rule_id="python.flask.add_url_rule",
|
|
84
|
+
kind=EntrypointKind.HTTP_ROUTE,
|
|
85
|
+
handler_qualified_name=handler,
|
|
86
|
+
route=route if route is not None else "",
|
|
87
|
+
http_methods=["*"],
|
|
88
|
+
framework="flask",
|
|
89
|
+
metadata={"registration": ref.arg_preview},
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
return hints
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _middleware(x: FileExtraction) -> list[EntrypointHint]:
|
|
96
|
+
"""before_request / after_request / errorhandler / app.middleware wrappers."""
|
|
97
|
+
hints = []
|
|
98
|
+
for symbol, decorator in _decorated(x):
|
|
99
|
+
m = _MIDDLEWARE_DECORATOR.match(decorator)
|
|
100
|
+
if m:
|
|
101
|
+
framework = "fastapi" if m.group(2) == "middleware" else "flask"
|
|
102
|
+
hints.append(
|
|
103
|
+
EntrypointHint(
|
|
104
|
+
rule_id="python.web.middleware",
|
|
105
|
+
kind=EntrypointKind.MIDDLEWARE,
|
|
106
|
+
handler_qualified_name=symbol.qualified_name,
|
|
107
|
+
name=m.group(2),
|
|
108
|
+
framework=framework,
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
return hints
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _fastapi_routes(x: FileExtraction) -> list[EntrypointHint]:
|
|
115
|
+
hints = []
|
|
116
|
+
for symbol, decorator in _decorated(x):
|
|
117
|
+
verb = _HTTP_VERB.match(decorator)
|
|
118
|
+
if verb:
|
|
119
|
+
route = first_string_arg(decorator)
|
|
120
|
+
if route is not None:
|
|
121
|
+
hints.append(
|
|
122
|
+
EntrypointHint(
|
|
123
|
+
rule_id="python.fastapi.route",
|
|
124
|
+
kind=EntrypointKind.HTTP_ROUTE,
|
|
125
|
+
handler_qualified_name=symbol.qualified_name,
|
|
126
|
+
route=route,
|
|
127
|
+
http_methods=[verb.group(2).upper()],
|
|
128
|
+
framework="fastapi",
|
|
129
|
+
metadata=_taint_meta(symbol, "http_route"),
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
return hints
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _django_urls(x: FileExtraction) -> list[EntrypointHint]:
|
|
136
|
+
if not x.path.endswith("urls.py"):
|
|
137
|
+
return []
|
|
138
|
+
hints = []
|
|
139
|
+
for ref in x.references:
|
|
140
|
+
if ref.kind == "call" and ref.callee_name in _DJANGO_PATH_CALL and ref.arg_preview:
|
|
141
|
+
route = first_string_arg("(" + ref.arg_preview.lstrip("("))
|
|
142
|
+
hints.append(
|
|
143
|
+
EntrypointHint(
|
|
144
|
+
rule_id="python.django.urls",
|
|
145
|
+
kind=EntrypointKind.HTTP_ROUTE,
|
|
146
|
+
handler_qualified_name=None, # handler resolved as a normal call edge
|
|
147
|
+
route=route if route is not None else "",
|
|
148
|
+
http_methods=["*"],
|
|
149
|
+
framework="django",
|
|
150
|
+
metadata={"registration": ref.arg_preview},
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
return hints
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _click_commands(x: FileExtraction) -> list[EntrypointHint]:
|
|
157
|
+
hints = []
|
|
158
|
+
for symbol, decorator in _decorated(x):
|
|
159
|
+
if _CLICK_CMD.match(decorator):
|
|
160
|
+
hints.append(
|
|
161
|
+
EntrypointHint(
|
|
162
|
+
rule_id="python.click.command",
|
|
163
|
+
kind=EntrypointKind.CLI_COMMAND,
|
|
164
|
+
handler_qualified_name=symbol.qualified_name,
|
|
165
|
+
name=first_string_arg(decorator) or symbol.name,
|
|
166
|
+
framework="click",
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
return hints
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _celery_tasks(x: FileExtraction) -> list[EntrypointHint]:
|
|
173
|
+
hints = []
|
|
174
|
+
for symbol, decorator in _decorated(x):
|
|
175
|
+
if _CELERY_TASK.match(decorator):
|
|
176
|
+
hints.append(
|
|
177
|
+
EntrypointHint(
|
|
178
|
+
rule_id="python.celery.task",
|
|
179
|
+
kind=EntrypointKind.TASK,
|
|
180
|
+
handler_qualified_name=symbol.qualified_name,
|
|
181
|
+
name=symbol.name,
|
|
182
|
+
framework="celery",
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
return hints
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _lambda_handlers(x: FileExtraction) -> list[EntrypointHint]:
|
|
189
|
+
hints = []
|
|
190
|
+
for symbol in x.symbols:
|
|
191
|
+
if (
|
|
192
|
+
symbol.kind is SymbolKind.FUNCTION
|
|
193
|
+
and symbol.name in _LAMBDA_NAMES
|
|
194
|
+
and symbol.signature
|
|
195
|
+
and "event" in symbol.signature
|
|
196
|
+
and "context" in symbol.signature
|
|
197
|
+
):
|
|
198
|
+
hints.append(
|
|
199
|
+
EntrypointHint(
|
|
200
|
+
rule_id="python.lambda.handler",
|
|
201
|
+
kind=EntrypointKind.LAMBDA_HANDLER,
|
|
202
|
+
handler_qualified_name=symbol.qualified_name,
|
|
203
|
+
name=symbol.name,
|
|
204
|
+
framework="aws-lambda",
|
|
205
|
+
metadata=_taint_meta(symbol, "lambda_handler"),
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
return hints
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
_VIEW_CONFIG = re.compile(r"^@view_config\b")
|
|
212
|
+
_DRAMATIQ_ACTOR = re.compile(r"^@(?:dramatiq\.actor|actor)\b")
|
|
213
|
+
_AIRFLOW_TASK = re.compile(r"^@(?:task|dag)\b")
|
|
214
|
+
_AIOHTTP_ADD = frozenset({"add_get", "add_post", "add_put", "add_delete", "add_route"})
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _sanic_bottle_routes(framework: str):
|
|
218
|
+
"""@app.route / @app.get style routes, framework-labelled (sanic, bottle)."""
|
|
219
|
+
|
|
220
|
+
def matcher(x: FileExtraction) -> list[EntrypointHint]:
|
|
221
|
+
hints = []
|
|
222
|
+
for symbol, decorator in _decorated(x):
|
|
223
|
+
route = methods = None
|
|
224
|
+
if _FLASK_ROUTE.match(decorator):
|
|
225
|
+
route, methods = first_string_arg(decorator), methods_kwarg(decorator) or ["GET"]
|
|
226
|
+
else:
|
|
227
|
+
verb = _HTTP_VERB.match(decorator)
|
|
228
|
+
if verb:
|
|
229
|
+
route, methods = first_string_arg(decorator), [verb.group(2).upper()]
|
|
230
|
+
if route is not None:
|
|
231
|
+
hints.append(EntrypointHint(
|
|
232
|
+
rule_id=f"python.{framework}.route", kind=EntrypointKind.HTTP_ROUTE,
|
|
233
|
+
handler_qualified_name=symbol.qualified_name, route=route,
|
|
234
|
+
http_methods=methods, framework=framework,
|
|
235
|
+
metadata=_taint_meta(symbol, "http_route")))
|
|
236
|
+
return hints
|
|
237
|
+
|
|
238
|
+
return matcher
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _decorator_rule(pattern, rule_id, kind, framework):
|
|
242
|
+
"""Simple decorator-matched entrypoint (pyramid view, dramatiq actor, airflow task)."""
|
|
243
|
+
|
|
244
|
+
def matcher(x: FileExtraction) -> list[EntrypointHint]:
|
|
245
|
+
hints = []
|
|
246
|
+
for symbol, decorator in _decorated(x):
|
|
247
|
+
if pattern.match(decorator):
|
|
248
|
+
hints.append(EntrypointHint(
|
|
249
|
+
rule_id=rule_id, kind=kind,
|
|
250
|
+
handler_qualified_name=symbol.qualified_name,
|
|
251
|
+
name=symbol.name, framework=framework,
|
|
252
|
+
route=first_string_arg(decorator) if kind is EntrypointKind.HTTP_ROUTE else None,
|
|
253
|
+
metadata=_taint_meta(symbol, kind.value)))
|
|
254
|
+
return hints
|
|
255
|
+
|
|
256
|
+
return matcher
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _aiohttp_routes(x: FileExtraction) -> list[EntrypointHint]:
|
|
260
|
+
"""router.add_get('/x', handler) / @routes.get('/x') registrations."""
|
|
261
|
+
hints = []
|
|
262
|
+
for ref in x.references:
|
|
263
|
+
if ref.kind == "call" and ref.callee_name in _AIOHTTP_ADD and ref.arg_preview:
|
|
264
|
+
route = first_string_arg("(" + ref.arg_preview.lstrip("("))
|
|
265
|
+
hints.append(EntrypointHint(
|
|
266
|
+
rule_id="python.aiohttp.route", kind=EntrypointKind.HTTP_ROUTE,
|
|
267
|
+
handler_qualified_name=None, route=route or "",
|
|
268
|
+
http_methods=[ref.callee_name.replace("add_", "").upper()], framework="aiohttp",
|
|
269
|
+
metadata={"registration": ref.arg_preview}))
|
|
270
|
+
for symbol, decorator in _decorated(x):
|
|
271
|
+
verb = _HTTP_VERB.match(decorator.replace("@routes.", "@r."))
|
|
272
|
+
if decorator.startswith("@routes.") and verb:
|
|
273
|
+
route = first_string_arg(decorator)
|
|
274
|
+
if route is not None:
|
|
275
|
+
hints.append(EntrypointHint(
|
|
276
|
+
rule_id="python.aiohttp.route", kind=EntrypointKind.HTTP_ROUTE,
|
|
277
|
+
handler_qualified_name=symbol.qualified_name, route=route,
|
|
278
|
+
http_methods=[verb.group(2).upper()], framework="aiohttp",
|
|
279
|
+
metadata=_taint_meta(symbol, "http_route")))
|
|
280
|
+
return hints
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _main_guard(x: FileExtraction) -> list[EntrypointHint]:
|
|
284
|
+
if ("main_guard", x.module_path) not in x.framework_signals:
|
|
285
|
+
return []
|
|
286
|
+
return [
|
|
287
|
+
EntrypointHint(
|
|
288
|
+
rule_id="python.core.main",
|
|
289
|
+
kind=EntrypointKind.MAIN,
|
|
290
|
+
handler_qualified_name=None, # the module itself
|
|
291
|
+
name=x.module_path,
|
|
292
|
+
)
|
|
293
|
+
]
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
register(EntrypointRule("python.flask.route", "python", "flask",
|
|
297
|
+
EntrypointKind.HTTP_ROUTE, _flask_routes))
|
|
298
|
+
register(EntrypointRule("python.flask.add_url_rule", "python", "flask",
|
|
299
|
+
EntrypointKind.HTTP_ROUTE, _flask_add_url_rule))
|
|
300
|
+
register(EntrypointRule("python.flask.middleware", "python", "flask",
|
|
301
|
+
EntrypointKind.MIDDLEWARE, _middleware))
|
|
302
|
+
register(EntrypointRule("python.fastapi.middleware", "python", "fastapi",
|
|
303
|
+
EntrypointKind.MIDDLEWARE, _middleware))
|
|
304
|
+
register(EntrypointRule("python.fastapi.route", "python", "fastapi",
|
|
305
|
+
EntrypointKind.HTTP_ROUTE, _fastapi_routes))
|
|
306
|
+
register(EntrypointRule("python.django.urls", "python", "django",
|
|
307
|
+
EntrypointKind.HTTP_ROUTE, _django_urls))
|
|
308
|
+
register(EntrypointRule("python.click.command", "python", "click",
|
|
309
|
+
EntrypointKind.CLI_COMMAND, _click_commands))
|
|
310
|
+
register(EntrypointRule("python.typer.command", "python", "typer",
|
|
311
|
+
EntrypointKind.CLI_COMMAND, _click_commands))
|
|
312
|
+
register(EntrypointRule("python.celery.task", "python", "celery",
|
|
313
|
+
EntrypointKind.TASK, _celery_tasks))
|
|
314
|
+
register(EntrypointRule("python.lambda.handler", "python", "aws-lambda",
|
|
315
|
+
EntrypointKind.LAMBDA_HANDLER, _lambda_handlers))
|
|
316
|
+
register(EntrypointRule("python.sanic.route", "python", "sanic",
|
|
317
|
+
EntrypointKind.HTTP_ROUTE, _sanic_bottle_routes("sanic")))
|
|
318
|
+
register(EntrypointRule("python.bottle.route", "python", "bottle",
|
|
319
|
+
EntrypointKind.HTTP_ROUTE, _sanic_bottle_routes("bottle")))
|
|
320
|
+
register(EntrypointRule("python.aiohttp.route", "python", "aiohttp",
|
|
321
|
+
EntrypointKind.HTTP_ROUTE, _aiohttp_routes))
|
|
322
|
+
register(EntrypointRule("python.pyramid.view", "python", "pyramid",
|
|
323
|
+
EntrypointKind.HTTP_ROUTE,
|
|
324
|
+
_decorator_rule(_VIEW_CONFIG, "python.pyramid.view",
|
|
325
|
+
EntrypointKind.HTTP_ROUTE, "pyramid")))
|
|
326
|
+
register(EntrypointRule("python.dramatiq.actor", "python", "dramatiq",
|
|
327
|
+
EntrypointKind.TASK,
|
|
328
|
+
_decorator_rule(_DRAMATIQ_ACTOR, "python.dramatiq.actor",
|
|
329
|
+
EntrypointKind.TASK, "dramatiq")))
|
|
330
|
+
register(EntrypointRule("python.airflow.task", "python", "airflow",
|
|
331
|
+
EntrypointKind.TASK,
|
|
332
|
+
_decorator_rule(_AIRFLOW_TASK, "python.airflow.task",
|
|
333
|
+
EntrypointKind.TASK, "airflow")))
|
|
334
|
+
register(EntrypointRule("python.core.main", "python", None,
|
|
335
|
+
EntrypointKind.MAIN, _main_guard))
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Ruby entrypoint rules: Sinatra routes, Rails route DSL, and Rake tasks.
|
|
2
|
+
|
|
3
|
+
Ruby route DSLs are bare top-level method calls (``get '/x' do ... end``), not
|
|
4
|
+
decorators, so these rules match against extracted call references rather than
|
|
5
|
+
symbol decorators. Sinatra handlers are inline blocks with no named symbol, so
|
|
6
|
+
the handler qualified name is the enclosing method (usually None at file top
|
|
7
|
+
level) — the block body's calls still resolve as ordinary edges.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from entrygraph.detect.entrypoints.base import (
|
|
13
|
+
EntrypointRule,
|
|
14
|
+
first_string_arg,
|
|
15
|
+
register,
|
|
16
|
+
)
|
|
17
|
+
from entrygraph.extract.ir import EntrypointHint, FileExtraction
|
|
18
|
+
from entrygraph.kinds import EntrypointKind
|
|
19
|
+
|
|
20
|
+
_SINATRA_VERBS = frozenset({"get", "post", "put", "delete", "patch"})
|
|
21
|
+
_RAILS_VERBS = frozenset({"get", "post", "put", "patch", "delete", "resources", "resource", "root"})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _sinatra_routes(x: FileExtraction) -> list[EntrypointHint]:
|
|
25
|
+
hints = []
|
|
26
|
+
for ref in x.references:
|
|
27
|
+
if (
|
|
28
|
+
ref.kind == "call"
|
|
29
|
+
and ref.receiver_text is None
|
|
30
|
+
and ref.callee_name in _SINATRA_VERBS
|
|
31
|
+
and ref.arg_preview
|
|
32
|
+
):
|
|
33
|
+
route = first_string_arg("(" + ref.arg_preview.lstrip("("))
|
|
34
|
+
if route is not None and route.startswith("/"):
|
|
35
|
+
hints.append(
|
|
36
|
+
EntrypointHint(
|
|
37
|
+
rule_id="ruby.sinatra.route",
|
|
38
|
+
kind=EntrypointKind.HTTP_ROUTE,
|
|
39
|
+
handler_qualified_name=ref.caller_qualified_name,
|
|
40
|
+
route=route,
|
|
41
|
+
http_methods=[ref.callee_name.upper()],
|
|
42
|
+
framework="sinatra",
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
return hints
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _rails_routes(x: FileExtraction) -> list[EntrypointHint]:
|
|
49
|
+
if not x.path.endswith("config/routes.rb"):
|
|
50
|
+
return []
|
|
51
|
+
hints = []
|
|
52
|
+
for ref in x.references:
|
|
53
|
+
if ref.kind != "call" or ref.receiver_text is not None:
|
|
54
|
+
continue
|
|
55
|
+
if ref.callee_name not in _RAILS_VERBS:
|
|
56
|
+
continue
|
|
57
|
+
route = None
|
|
58
|
+
if ref.arg_preview:
|
|
59
|
+
route = first_string_arg("(" + ref.arg_preview.lstrip("("))
|
|
60
|
+
hints.append(
|
|
61
|
+
EntrypointHint(
|
|
62
|
+
rule_id="ruby.rails.routes",
|
|
63
|
+
kind=EntrypointKind.HTTP_ROUTE,
|
|
64
|
+
handler_qualified_name=None, # handler resolved as a normal call edge
|
|
65
|
+
route=route if route is not None else "",
|
|
66
|
+
http_methods=["*"] if ref.callee_name in ("resources", "resource", "root")
|
|
67
|
+
else [ref.callee_name.upper()],
|
|
68
|
+
framework="rails",
|
|
69
|
+
metadata={"registration": ref.arg_preview or ref.callee_name},
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
return hints
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _rake_tasks(x: FileExtraction) -> list[EntrypointHint]:
|
|
76
|
+
hints = []
|
|
77
|
+
for ref in x.references:
|
|
78
|
+
if ref.kind == "call" and ref.receiver_text is None and ref.callee_name == "task":
|
|
79
|
+
name = None
|
|
80
|
+
if ref.arg_preview:
|
|
81
|
+
name = first_string_arg("(" + ref.arg_preview.lstrip("("))
|
|
82
|
+
if name is None:
|
|
83
|
+
# symbol arg like `:build` — strip the leading colon
|
|
84
|
+
stripped = ref.arg_preview.strip("()").lstrip(":").split(" ", 1)[0]
|
|
85
|
+
stripped = stripped.split("=", 1)[0].strip().rstrip(",")
|
|
86
|
+
name = stripped or None
|
|
87
|
+
hints.append(
|
|
88
|
+
EntrypointHint(
|
|
89
|
+
rule_id="ruby.rake.task",
|
|
90
|
+
kind=EntrypointKind.CLI_COMMAND,
|
|
91
|
+
handler_qualified_name=ref.caller_qualified_name,
|
|
92
|
+
name=name or "task",
|
|
93
|
+
framework="rake",
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
return hints
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _grape_routes(x: FileExtraction) -> list[EntrypointHint]:
|
|
100
|
+
"""Grape API classes: class-body `get '/x'` / `post '/y'` declarations."""
|
|
101
|
+
hints = []
|
|
102
|
+
for ref in x.references:
|
|
103
|
+
if (
|
|
104
|
+
ref.kind == "call"
|
|
105
|
+
and ref.receiver_text is None
|
|
106
|
+
and ref.callee_name in _SINATRA_VERBS
|
|
107
|
+
and ref.arg_preview
|
|
108
|
+
):
|
|
109
|
+
route = first_string_arg("(" + ref.arg_preview.lstrip("("))
|
|
110
|
+
if route is not None:
|
|
111
|
+
hints.append(EntrypointHint(
|
|
112
|
+
rule_id="ruby.grape.route", kind=EntrypointKind.HTTP_ROUTE,
|
|
113
|
+
handler_qualified_name=ref.caller_qualified_name,
|
|
114
|
+
route=route, http_methods=[ref.callee_name.upper()], framework="grape"))
|
|
115
|
+
return hints
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _sidekiq_workers(x: FileExtraction) -> list[EntrypointHint]:
|
|
119
|
+
"""Classes including Sidekiq::Worker/Job expose `perform` as a task handler."""
|
|
120
|
+
worker_modules = {
|
|
121
|
+
ref.caller_qualified_name
|
|
122
|
+
for ref in x.references
|
|
123
|
+
if ref.callee_name == "include" and ref.arg_preview
|
|
124
|
+
and "Sidekiq" in ref.arg_preview
|
|
125
|
+
}
|
|
126
|
+
hints = []
|
|
127
|
+
for symbol in x.symbols:
|
|
128
|
+
if symbol.name == "perform" and (
|
|
129
|
+
symbol.parent_qualified_name in worker_modules or not worker_modules
|
|
130
|
+
) and worker_modules:
|
|
131
|
+
hints.append(EntrypointHint(
|
|
132
|
+
rule_id="ruby.sidekiq.worker", kind=EntrypointKind.TASK,
|
|
133
|
+
handler_qualified_name=symbol.qualified_name,
|
|
134
|
+
name=symbol.parent_qualified_name or symbol.name, framework="sidekiq"))
|
|
135
|
+
return hints
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
register(EntrypointRule("ruby.sinatra.route", "ruby", "sinatra",
|
|
139
|
+
EntrypointKind.HTTP_ROUTE, _sinatra_routes))
|
|
140
|
+
register(EntrypointRule("ruby.grape.route", "ruby", "grape",
|
|
141
|
+
EntrypointKind.HTTP_ROUTE, _grape_routes))
|
|
142
|
+
register(EntrypointRule("ruby.sidekiq.worker", "ruby", "sidekiq",
|
|
143
|
+
EntrypointKind.TASK, _sidekiq_workers))
|
|
144
|
+
register(EntrypointRule("ruby.rails.routes", "ruby", "rails",
|
|
145
|
+
EntrypointKind.HTTP_ROUTE, _rails_routes))
|
|
146
|
+
register(EntrypointRule("ruby.rake.task", "ruby", "rake",
|
|
147
|
+
EntrypointKind.CLI_COMMAND, _rake_tasks))
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Rust entrypoint rules: the crate ``main`` (and ``#[tokio::main]`` /
|
|
2
|
+
``#[actix_web::main]`` async mains), axum router routes, actix-web/rocket route
|
|
3
|
+
attribute macros, and clap ``#[derive(Parser)]`` CLI commands.
|
|
4
|
+
|
|
5
|
+
Rules are IR-driven: axum routes read ``route(...)`` call references (the same
|
|
6
|
+
low-fidelity tradeoff as the Express rule — the handler lives inside the second
|
|
7
|
+
argument, so ``handler_qualified_name`` is left None and the raw registration is
|
|
8
|
+
stashed in ``metadata``). actix/rocket/clap rules read attribute macros captured
|
|
9
|
+
on ``RawSymbol.decorators`` as raw source text (``#[get("/x")]``).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
|
|
16
|
+
from entrygraph.detect.entrypoints.base import (
|
|
17
|
+
EntrypointRule,
|
|
18
|
+
first_string_arg,
|
|
19
|
+
register,
|
|
20
|
+
)
|
|
21
|
+
from entrygraph.extract.ir import EntrypointHint, FileExtraction, RawSymbol
|
|
22
|
+
from entrygraph.kinds import EntrypointKind, SymbolKind
|
|
23
|
+
|
|
24
|
+
# #[get("/x")] / #[post(..)] / ... on a handler function -> HTTP verb.
|
|
25
|
+
_ROUTE_ATTR = re.compile(r"^#\[\s*(get|post|put|delete|patch|head|route)\s*\(")
|
|
26
|
+
_VERB = {
|
|
27
|
+
"get": "GET", "post": "POST", "put": "PUT", "delete": "DELETE",
|
|
28
|
+
"patch": "PATCH", "head": "HEAD",
|
|
29
|
+
}
|
|
30
|
+
# #[derive(.. Parser ..)] anywhere in the derive list.
|
|
31
|
+
_DERIVE_PARSER = re.compile(r"^#\[\s*derive\s*\(.*\bParser\b")
|
|
32
|
+
# #[tokio::main] / #[actix_web::main] async entrypoints.
|
|
33
|
+
_ASYNC_MAIN = re.compile(r"^#\[\s*(?:tokio|actix_web|async_std)::main\b")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _rust_main(x: FileExtraction) -> list[EntrypointHint]:
|
|
37
|
+
hints = []
|
|
38
|
+
for symbol in x.symbols:
|
|
39
|
+
if symbol.kind is not SymbolKind.FUNCTION:
|
|
40
|
+
continue
|
|
41
|
+
is_crate_main = symbol.name == "main" and symbol.parent_qualified_name is None
|
|
42
|
+
is_async_main = any(_ASYNC_MAIN.match(d) for d in symbol.decorators)
|
|
43
|
+
if is_crate_main or is_async_main:
|
|
44
|
+
hints.append(
|
|
45
|
+
EntrypointHint(
|
|
46
|
+
rule_id="rust.core.main",
|
|
47
|
+
kind=EntrypointKind.MAIN,
|
|
48
|
+
handler_qualified_name=symbol.qualified_name,
|
|
49
|
+
name=symbol.qualified_name,
|
|
50
|
+
span=symbol.span,
|
|
51
|
+
framework=None,
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
return hints
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _axum_routes(x: FileExtraction) -> list[EntrypointHint]:
|
|
58
|
+
"""`Router::new().route("/x", post(handler))` — the handler is buried in the
|
|
59
|
+
second argument, so we regex it out of the preview but can't statically bind
|
|
60
|
+
it (the Express-rule fidelity tradeoff)."""
|
|
61
|
+
hints = []
|
|
62
|
+
for ref in x.references:
|
|
63
|
+
if ref.kind != "call" or ref.callee_name != "route" or not ref.arg_preview:
|
|
64
|
+
continue
|
|
65
|
+
route = first_string_arg("(" + ref.arg_preview.lstrip("("))
|
|
66
|
+
if route is None or not route.startswith("/"):
|
|
67
|
+
continue
|
|
68
|
+
method, handler = _axum_method_and_handler(ref.arg_preview)
|
|
69
|
+
hints.append(
|
|
70
|
+
EntrypointHint(
|
|
71
|
+
rule_id="rust.axum.route",
|
|
72
|
+
kind=EntrypointKind.HTTP_ROUTE,
|
|
73
|
+
handler_qualified_name=None,
|
|
74
|
+
route=route,
|
|
75
|
+
http_methods=[method],
|
|
76
|
+
framework="axum",
|
|
77
|
+
metadata={"handler_text": handler, "registration": ref.arg_preview},
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
return hints
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
_AXUM_HANDLER = re.compile(
|
|
84
|
+
r"\b(get|post|put|delete|patch|head|options)\s*\(\s*([A-Za-z_]\w*)"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _axum_method_and_handler(preview: str) -> tuple[str, str | None]:
|
|
89
|
+
match = _AXUM_HANDLER.search(preview)
|
|
90
|
+
if match:
|
|
91
|
+
return match.group(1).upper(), match.group(2)
|
|
92
|
+
return "*", None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _route_attr_rule(framework: str):
|
|
96
|
+
"""actix-web and rocket both register routes with `#[get("/x")]`-style
|
|
97
|
+
attribute macros on the handler function; one matcher, framework by param."""
|
|
98
|
+
|
|
99
|
+
def matcher(x: FileExtraction) -> list[EntrypointHint]:
|
|
100
|
+
hints = []
|
|
101
|
+
for symbol in x.symbols:
|
|
102
|
+
if symbol.kind not in (SymbolKind.FUNCTION, SymbolKind.METHOD):
|
|
103
|
+
continue
|
|
104
|
+
for decorator in symbol.decorators:
|
|
105
|
+
m = _ROUTE_ATTR.match(decorator)
|
|
106
|
+
if not m:
|
|
107
|
+
continue
|
|
108
|
+
verb = _VERB.get(m.group(1), "*")
|
|
109
|
+
hints.append(
|
|
110
|
+
EntrypointHint(
|
|
111
|
+
rule_id=f"rust.{framework}.route",
|
|
112
|
+
kind=EntrypointKind.HTTP_ROUTE,
|
|
113
|
+
handler_qualified_name=symbol.qualified_name,
|
|
114
|
+
route=first_string_arg(decorator) or "",
|
|
115
|
+
http_methods=[verb],
|
|
116
|
+
framework=framework,
|
|
117
|
+
span=symbol.span,
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
return hints
|
|
121
|
+
|
|
122
|
+
return matcher
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _clap_commands(x: FileExtraction) -> list[EntrypointHint]:
|
|
126
|
+
hints = []
|
|
127
|
+
for symbol in x.symbols:
|
|
128
|
+
if symbol.kind not in (SymbolKind.STRUCT, SymbolKind.CLASS):
|
|
129
|
+
continue
|
|
130
|
+
if any(_DERIVE_PARSER.match(d) for d in symbol.decorators):
|
|
131
|
+
hints.append(
|
|
132
|
+
EntrypointHint(
|
|
133
|
+
rule_id="rust.clap.command",
|
|
134
|
+
kind=EntrypointKind.CLI_COMMAND,
|
|
135
|
+
handler_qualified_name=symbol.qualified_name,
|
|
136
|
+
name=symbol.name,
|
|
137
|
+
framework="clap",
|
|
138
|
+
span=symbol.span,
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
return hints
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
register(EntrypointRule("rust.core.main", "rust", None,
|
|
145
|
+
EntrypointKind.MAIN, _rust_main))
|
|
146
|
+
register(EntrypointRule("rust.axum.route", "rust", "axum",
|
|
147
|
+
EntrypointKind.HTTP_ROUTE, _axum_routes))
|
|
148
|
+
register(EntrypointRule("rust.actix.route", "rust", "actix-web",
|
|
149
|
+
EntrypointKind.HTTP_ROUTE, _route_attr_rule("actix-web")))
|
|
150
|
+
register(EntrypointRule("rust.rocket.route", "rust", "rocket",
|
|
151
|
+
EntrypointKind.HTTP_ROUTE, _route_attr_rule("rocket")))
|
|
152
|
+
register(EntrypointRule("rust.clap.command", "rust", "clap",
|
|
153
|
+
EntrypointKind.CLI_COMMAND, _clap_commands))
|