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,124 @@
|
|
|
1
|
+
"""Entrypoint rule schema and registry.
|
|
2
|
+
|
|
3
|
+
Rules are IR-driven: they match against a FileExtraction (symbols, decorators,
|
|
4
|
+
references, framework signals) rather than re-walking the tree, which keeps
|
|
5
|
+
them cheap and language-uniform. A rule gated on a framework only runs when
|
|
6
|
+
that framework was detected above threshold — `app.get("/x")` in random code
|
|
7
|
+
is not an Express route.
|
|
8
|
+
|
|
9
|
+
Adding a framework = registering one rule (and usually one FrameworkSpec).
|
|
10
|
+
Third parties: entrygraph.detect.entrypoints.register(rule).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Callable
|
|
18
|
+
|
|
19
|
+
from entrygraph.extract.ir import EntrypointHint, FileExtraction
|
|
20
|
+
from entrygraph.kinds import EntrypointKind
|
|
21
|
+
|
|
22
|
+
Matcher = Callable[[FileExtraction], list[EntrypointHint]]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class EntrypointRule:
|
|
27
|
+
id: str # "python.flask.route"
|
|
28
|
+
language: str
|
|
29
|
+
framework: str | None # None => language-core rule, always runs
|
|
30
|
+
kind: EntrypointKind
|
|
31
|
+
match: Matcher
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_RULES: list[EntrypointRule] = []
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def register(rule: EntrypointRule) -> None:
|
|
38
|
+
_RULES.append(rule)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# TypeScript/TSX reuse the JavaScript rule set (one extractor drives all three).
|
|
42
|
+
_LANG_ALIASES = {"typescript": "javascript", "tsx": "javascript"}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def rules_for(language: str, detected_frameworks: set[str]) -> list[EntrypointRule]:
|
|
46
|
+
lang = _LANG_ALIASES.get(language, language)
|
|
47
|
+
return [
|
|
48
|
+
r
|
|
49
|
+
for r in _RULES
|
|
50
|
+
if r.language == lang
|
|
51
|
+
and (r.framework is None or r.framework in detected_frameworks)
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def all_rules() -> list[EntrypointRule]:
|
|
56
|
+
return list(_RULES)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------------- shared decorator-parsing helpers ----------------
|
|
60
|
+
|
|
61
|
+
_STRING_ARG = re.compile(r"""\(\s*[rbf]*["']([^"']+)["']""")
|
|
62
|
+
_METHODS_KWARG = re.compile(r"""methods\s*=\s*[\[(]([^\])]*)[\])]""")
|
|
63
|
+
_QUOTED = re.compile(r"""["']([^"']+)["']""")
|
|
64
|
+
_IDENTIFIER = re.compile(r"[A-Za-z_]\w*")
|
|
65
|
+
_PARAMS = re.compile(r"\(([^)]*)\)")
|
|
66
|
+
|
|
67
|
+
# names conventionally carrying user-controlled input across web frameworks
|
|
68
|
+
_TAINTED_NAMES = frozenset(
|
|
69
|
+
{"request", "req", "params", "event", "body", "payload", "query", "form", "data"}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def first_string_arg(decorator_text: str) -> str | None:
|
|
74
|
+
match = _STRING_ARG.search(decorator_text)
|
|
75
|
+
return match.group(1) if match else None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def methods_kwarg(decorator_text: str) -> list[str]:
|
|
79
|
+
match = _METHODS_KWARG.search(decorator_text)
|
|
80
|
+
if not match:
|
|
81
|
+
return []
|
|
82
|
+
return [m.upper() for m in _QUOTED.findall(match.group(1))]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def identifier_args(arg_preview: str | None) -> list[str]:
|
|
86
|
+
"""Bare identifiers appearing as call arguments (best-effort over the preview).
|
|
87
|
+
|
|
88
|
+
Skips string/number literals and keyword-argument names before '='.
|
|
89
|
+
"""
|
|
90
|
+
if not arg_preview:
|
|
91
|
+
return []
|
|
92
|
+
names: list[str] = []
|
|
93
|
+
for part in arg_preview.strip("()").split(","):
|
|
94
|
+
token = part.strip()
|
|
95
|
+
if "=" in token: # keyword arg: take the value side only
|
|
96
|
+
token = token.split("=", 1)[1].strip()
|
|
97
|
+
if token and _IDENTIFIER.fullmatch(token):
|
|
98
|
+
names.append(token)
|
|
99
|
+
return names
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def tainted_params(signature: str | None, kind: str) -> list[str]:
|
|
103
|
+
"""Parameters of an entrypoint handler that are likely user-controlled.
|
|
104
|
+
|
|
105
|
+
HTTP handlers: every parameter except self/cls (path/query params flow in).
|
|
106
|
+
Lambda handlers: the `event` parameter. Otherwise: parameters whose names
|
|
107
|
+
match the conventional tainted-name set.
|
|
108
|
+
"""
|
|
109
|
+
if not signature:
|
|
110
|
+
return []
|
|
111
|
+
match = _PARAMS.search(signature)
|
|
112
|
+
if not match:
|
|
113
|
+
return []
|
|
114
|
+
raw = [p.strip() for p in match.group(1).split(",") if p.strip()]
|
|
115
|
+
params = []
|
|
116
|
+
for p in raw:
|
|
117
|
+
name = p.split(":", 1)[0].split("=", 1)[0].strip().lstrip("*")
|
|
118
|
+
if name and name not in ("self", "cls"):
|
|
119
|
+
params.append(name)
|
|
120
|
+
if kind == "http_route":
|
|
121
|
+
return params
|
|
122
|
+
if kind == "lambda_handler":
|
|
123
|
+
return [p for p in params if p == "event"] or params[:1]
|
|
124
|
+
return [p for p in params if p in _TAINTED_NAMES]
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Config-file entrypoints — handlers declared outside source code.
|
|
2
|
+
|
|
3
|
+
The walker only feeds source files to extractors, so serverless/SAM handler
|
|
4
|
+
fields, Procfile process lines, and Dockerfile CMD/ENTRYPOINT never reach the
|
|
5
|
+
IR-driven rules. This module scans a handful of well-known root files with the
|
|
6
|
+
same stdlib+regex approach as detect/manifests.py (there is no YAML dependency),
|
|
7
|
+
producing ConfigHints that bind to a real symbol where possible.
|
|
8
|
+
|
|
9
|
+
`Entrypoint.symbol_id` is NOT NULL, so a hint whose handler cannot be bound to
|
|
10
|
+
an indexed symbol is dropped rather than synthesizing a placeholder symbol.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from entrygraph.kinds import EntrypointKind
|
|
20
|
+
|
|
21
|
+
_SERVERLESS_HANDLER = re.compile(r"^\s*handler:\s*([^\s#]+)", re.MULTILINE)
|
|
22
|
+
_SAM_HANDLER = re.compile(r"^\s*Handler:\s*([^\s#]+)", re.MULTILINE)
|
|
23
|
+
_PROCFILE_LINE = re.compile(r"^(\w+):\s*(.+)$", re.MULTILINE)
|
|
24
|
+
_DOCKER_CMD = re.compile(r"^\s*(?:CMD|ENTRYPOINT)\s+(.+)$", re.MULTILINE | re.IGNORECASE)
|
|
25
|
+
|
|
26
|
+
# frameworks whose entrypoints are re-derived from config each index run
|
|
27
|
+
CONFIG_FRAMEWORKS = ("serverless", "sam", "procfile", "docker")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(slots=True)
|
|
31
|
+
class ConfigHint:
|
|
32
|
+
kind: EntrypointKind
|
|
33
|
+
framework: str
|
|
34
|
+
handler_ref: str # raw handler string as written in the config
|
|
35
|
+
name: str
|
|
36
|
+
route: str | None = None
|
|
37
|
+
metadata: dict = field(default_factory=dict)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def scan_config_entrypoints(root: str | Path) -> list[ConfigHint]:
|
|
41
|
+
root = Path(root)
|
|
42
|
+
hints: list[ConfigHint] = []
|
|
43
|
+
for name in ("serverless.yml", "serverless.yaml"):
|
|
44
|
+
hints += _scan(root / name, _SERVERLESS_HANDLER, EntrypointKind.LAMBDA_HANDLER, "serverless")
|
|
45
|
+
for name in ("template.yaml", "template.yml"):
|
|
46
|
+
hints += _scan(root / name, _SAM_HANDLER, EntrypointKind.LAMBDA_HANDLER, "sam")
|
|
47
|
+
hints += _scan_procfile(root / "Procfile")
|
|
48
|
+
hints += _scan_dockerfile(root / "Dockerfile")
|
|
49
|
+
return hints
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _read(path: Path) -> str | None:
|
|
53
|
+
if not path.is_file():
|
|
54
|
+
return None
|
|
55
|
+
try:
|
|
56
|
+
return path.read_text(encoding="utf-8", errors="replace")
|
|
57
|
+
except OSError: # pragma: no cover
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _scan(path: Path, pattern: re.Pattern, kind: EntrypointKind, framework: str) -> list[ConfigHint]:
|
|
62
|
+
text = _read(path)
|
|
63
|
+
if text is None:
|
|
64
|
+
return []
|
|
65
|
+
hints = []
|
|
66
|
+
for match in pattern.finditer(text):
|
|
67
|
+
ref = match.group(1).strip().strip("'\"")
|
|
68
|
+
hints.append(ConfigHint(kind=kind, framework=framework, handler_ref=ref, name=ref))
|
|
69
|
+
return hints
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _scan_procfile(path: Path) -> list[ConfigHint]:
|
|
73
|
+
text = _read(path)
|
|
74
|
+
if text is None:
|
|
75
|
+
return []
|
|
76
|
+
hints = []
|
|
77
|
+
for proc, command in _PROCFILE_LINE.findall(text):
|
|
78
|
+
hints.append(ConfigHint(kind=EntrypointKind.MAIN, framework="procfile",
|
|
79
|
+
handler_ref=command.strip(), name=proc))
|
|
80
|
+
return hints
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _scan_dockerfile(path: Path) -> list[ConfigHint]:
|
|
84
|
+
text = _read(path)
|
|
85
|
+
if text is None:
|
|
86
|
+
return []
|
|
87
|
+
hints = []
|
|
88
|
+
for command in _DOCKER_CMD.findall(text):
|
|
89
|
+
hints.append(ConfigHint(kind=EntrypointKind.MAIN, framework="docker",
|
|
90
|
+
handler_ref=command.strip(), name="cmd"))
|
|
91
|
+
return hints
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def bind_handler(handler_ref: str, symbol_id_by_qname: dict[str, int],
|
|
95
|
+
module_symbol_ids: dict[str, int]) -> int | None:
|
|
96
|
+
"""Best-effort map a config handler string to an indexed symbol id.
|
|
97
|
+
|
|
98
|
+
Handles the common forms:
|
|
99
|
+
- ``src/app.handler`` / ``app.lambda_handler`` (dotted or path+dot)
|
|
100
|
+
- ``pkg.mod:app`` (gunicorn/uvicorn colon form)
|
|
101
|
+
- ``python -m pkg.mod`` / ``node src/server.js`` (process commands)
|
|
102
|
+
"""
|
|
103
|
+
for qname in _candidate_qnames(handler_ref):
|
|
104
|
+
if qname in symbol_id_by_qname:
|
|
105
|
+
return symbol_id_by_qname[qname]
|
|
106
|
+
if qname in module_symbol_ids:
|
|
107
|
+
return module_symbol_ids[qname]
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
_SRC_PREFIXES = ("src/", "lib/", "app/")
|
|
112
|
+
_CODE_EXTS = (".py", ".js", ".ts", ".mjs", ".cjs", ".rb", ".go")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _candidate_qnames(handler_ref: str) -> list[str]:
|
|
116
|
+
ref = handler_ref.strip()
|
|
117
|
+
# process command: take the token that looks like a module/path
|
|
118
|
+
if " " in ref:
|
|
119
|
+
tokens = ref.split()
|
|
120
|
+
if "-m" in tokens:
|
|
121
|
+
i = tokens.index("-m")
|
|
122
|
+
if i + 1 < len(tokens):
|
|
123
|
+
ref = tokens[i + 1]
|
|
124
|
+
else:
|
|
125
|
+
ref = next((t for t in tokens[1:] if "/" in t or "." in t), tokens[-1])
|
|
126
|
+
ref = ref.split(":", 1)[0] if ":" in ref and "/" not in ref.split(":", 1)[1] else ref.replace(":", ".")
|
|
127
|
+
for prefix in _SRC_PREFIXES:
|
|
128
|
+
if ref.startswith(prefix):
|
|
129
|
+
ref = ref[len(prefix):]
|
|
130
|
+
for ext in _CODE_EXTS:
|
|
131
|
+
if ref.endswith(ext):
|
|
132
|
+
ref = ref[: -len(ext)]
|
|
133
|
+
break
|
|
134
|
+
dotted = ref.replace("/", ".").strip(".")
|
|
135
|
+
candidates = [dotted]
|
|
136
|
+
# a handler like module.function -> also try the bare module
|
|
137
|
+
if "." in dotted:
|
|
138
|
+
candidates.append(dotted.rsplit(".", 1)[0])
|
|
139
|
+
return [c for c in candidates if c]
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""C# entrypoint rules: ASP.NET Core MVC/attribute-routed controllers, minimal
|
|
2
|
+
API route registrations, and the language-core ``static Main`` entrypoint.
|
|
3
|
+
|
|
4
|
+
Controller rules scan ``FileExtraction.symbols`` and their captured attributes
|
|
5
|
+
(stored on ``RawSymbol.decorators`` as raw source text, e.g.
|
|
6
|
+
``[HttpGet("/x")]``). A controller is a type carrying ``[ApiController]`` /
|
|
7
|
+
``[Controller]`` or whose base type name contains ``Controller``. Minimal-API
|
|
8
|
+
routes are recognised from ``MapGet``/``MapPost``/... call references.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
from entrygraph.detect.entrypoints.base import (
|
|
16
|
+
EntrypointRule,
|
|
17
|
+
first_string_arg,
|
|
18
|
+
register,
|
|
19
|
+
)
|
|
20
|
+
from entrygraph.extract.ir import EntrypointHint, FileExtraction, RawSymbol
|
|
21
|
+
from entrygraph.kinds import EntrypointKind, SymbolKind
|
|
22
|
+
|
|
23
|
+
# [HttpGet("/x")] / [HttpPost] / ... -> HTTP verb; [Route("...")] -> generic.
|
|
24
|
+
_HTTP_ATTR = re.compile(r"^\[Http(Get|Post|Put|Delete|Patch|Head|Options)\b")
|
|
25
|
+
_ROUTE_ATTR = re.compile(r"^\[Route\b")
|
|
26
|
+
_CONTROLLER_ATTR = re.compile(r"^\[(ApiController|Controller)\b")
|
|
27
|
+
|
|
28
|
+
_MINIMAL_MAP = {
|
|
29
|
+
"MapGet": "GET",
|
|
30
|
+
"MapPost": "POST",
|
|
31
|
+
"MapPut": "PUT",
|
|
32
|
+
"MapDelete": "DELETE",
|
|
33
|
+
"MapPatch": "PATCH",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _type_symbols(x: FileExtraction) -> list[RawSymbol]:
|
|
38
|
+
return [s for s in x.symbols if s.kind in (SymbolKind.CLASS, SymbolKind.INTERFACE)]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _methods(x: FileExtraction) -> list[RawSymbol]:
|
|
42
|
+
return [s for s in x.symbols if s.kind is SymbolKind.METHOD]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _is_controller(t: RawSymbol) -> bool:
|
|
46
|
+
if any(_CONTROLLER_ATTR.match(d) for d in t.decorators):
|
|
47
|
+
return True
|
|
48
|
+
return any("Controller" in b for b in t.bases)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _aspnet_controller_routes(x: FileExtraction) -> list[EntrypointHint]:
|
|
52
|
+
controllers = {t.qualified_name for t in _type_symbols(x) if _is_controller(t)}
|
|
53
|
+
if not controllers:
|
|
54
|
+
return []
|
|
55
|
+
hints: list[EntrypointHint] = []
|
|
56
|
+
for method in _methods(x):
|
|
57
|
+
if method.parent_qualified_name not in controllers:
|
|
58
|
+
continue
|
|
59
|
+
for decorator in method.decorators:
|
|
60
|
+
m = _HTTP_ATTR.match(decorator)
|
|
61
|
+
if m:
|
|
62
|
+
hints.append(
|
|
63
|
+
EntrypointHint(
|
|
64
|
+
rule_id="csharp.aspnet.controller-route",
|
|
65
|
+
kind=EntrypointKind.HTTP_ROUTE,
|
|
66
|
+
handler_qualified_name=method.qualified_name,
|
|
67
|
+
route=first_string_arg(decorator) or "",
|
|
68
|
+
http_methods=[m.group(1).upper()],
|
|
69
|
+
framework="aspnetcore",
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
break
|
|
73
|
+
else:
|
|
74
|
+
# No Http* verb attribute; a bare [Route("...")] still exposes it.
|
|
75
|
+
route_dec = next(
|
|
76
|
+
(d for d in method.decorators if _ROUTE_ATTR.match(d)), None
|
|
77
|
+
)
|
|
78
|
+
if route_dec is not None:
|
|
79
|
+
hints.append(
|
|
80
|
+
EntrypointHint(
|
|
81
|
+
rule_id="csharp.aspnet.controller-route",
|
|
82
|
+
kind=EntrypointKind.HTTP_ROUTE,
|
|
83
|
+
handler_qualified_name=method.qualified_name,
|
|
84
|
+
route=first_string_arg(route_dec) or "",
|
|
85
|
+
http_methods=["*"],
|
|
86
|
+
framework="aspnetcore",
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
return hints
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _aspnet_minimal_api(x: FileExtraction) -> list[EntrypointHint]:
|
|
93
|
+
hints: list[EntrypointHint] = []
|
|
94
|
+
for ref in x.references:
|
|
95
|
+
if ref.kind != "call":
|
|
96
|
+
continue
|
|
97
|
+
verb = _MINIMAL_MAP.get(ref.callee_name)
|
|
98
|
+
if verb is None:
|
|
99
|
+
continue
|
|
100
|
+
route = first_string_arg(ref.arg_preview or "")
|
|
101
|
+
hints.append(
|
|
102
|
+
EntrypointHint(
|
|
103
|
+
rule_id="csharp.aspnet.minimal-api",
|
|
104
|
+
kind=EntrypointKind.HTTP_ROUTE,
|
|
105
|
+
handler_qualified_name=None, # inline lambda handler
|
|
106
|
+
route=route or "",
|
|
107
|
+
http_methods=[verb],
|
|
108
|
+
framework="aspnetcore",
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
return hints
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _csharp_main(x: FileExtraction) -> list[EntrypointHint]:
|
|
115
|
+
hints: list[EntrypointHint] = []
|
|
116
|
+
for method in _methods(x):
|
|
117
|
+
if method.name == "Main" and "static" in method.modifiers:
|
|
118
|
+
hints.append(
|
|
119
|
+
EntrypointHint(
|
|
120
|
+
rule_id="csharp.core.main",
|
|
121
|
+
kind=EntrypointKind.MAIN,
|
|
122
|
+
handler_qualified_name=method.qualified_name,
|
|
123
|
+
name=method.qualified_name,
|
|
124
|
+
framework=None,
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
return hints
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
register(
|
|
131
|
+
EntrypointRule(
|
|
132
|
+
"csharp.aspnet.controller-route",
|
|
133
|
+
"csharp",
|
|
134
|
+
"aspnetcore",
|
|
135
|
+
EntrypointKind.HTTP_ROUTE,
|
|
136
|
+
_aspnet_controller_routes,
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
register(
|
|
140
|
+
EntrypointRule(
|
|
141
|
+
"csharp.aspnet.minimal-api",
|
|
142
|
+
"csharp",
|
|
143
|
+
"aspnetcore",
|
|
144
|
+
EntrypointKind.HTTP_ROUTE,
|
|
145
|
+
_aspnet_minimal_api,
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
register(
|
|
149
|
+
EntrypointRule(
|
|
150
|
+
"csharp.core.main",
|
|
151
|
+
"csharp",
|
|
152
|
+
None,
|
|
153
|
+
EntrypointKind.MAIN,
|
|
154
|
+
_csharp_main,
|
|
155
|
+
)
|
|
156
|
+
)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Go entrypoint rules: the package-main entrypoint, net/http handler
|
|
2
|
+
registration, gin router routes, and cobra CLI commands."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from entrygraph.detect.entrypoints.base import (
|
|
7
|
+
EntrypointRule,
|
|
8
|
+
first_string_arg,
|
|
9
|
+
register,
|
|
10
|
+
)
|
|
11
|
+
from entrygraph.extract.ir import EntrypointHint, FileExtraction
|
|
12
|
+
from entrygraph.kinds import EntrypointKind, SymbolKind
|
|
13
|
+
|
|
14
|
+
_GIN_METHODS = frozenset(
|
|
15
|
+
{"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "Any", "Handle"}
|
|
16
|
+
)
|
|
17
|
+
_NETHTTP_REGISTER = frozenset({"HandleFunc", "Handle"})
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _go_main(x: FileExtraction) -> list[EntrypointHint]:
|
|
21
|
+
hints = []
|
|
22
|
+
for symbol in x.symbols:
|
|
23
|
+
if (
|
|
24
|
+
symbol.kind is SymbolKind.FUNCTION
|
|
25
|
+
and symbol.name == "main"
|
|
26
|
+
and symbol.parent_qualified_name is None
|
|
27
|
+
):
|
|
28
|
+
hints.append(
|
|
29
|
+
EntrypointHint(
|
|
30
|
+
rule_id="go.core.main",
|
|
31
|
+
kind=EntrypointKind.MAIN,
|
|
32
|
+
handler_qualified_name=symbol.qualified_name,
|
|
33
|
+
name=symbol.qualified_name,
|
|
34
|
+
span=symbol.span,
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
return hints
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _nethttp_routes(x: FileExtraction) -> list[EntrypointHint]:
|
|
41
|
+
hints = []
|
|
42
|
+
for ref in x.references:
|
|
43
|
+
if (
|
|
44
|
+
ref.kind == "call"
|
|
45
|
+
and ref.receiver_text == "http"
|
|
46
|
+
and ref.callee_name in _NETHTTP_REGISTER
|
|
47
|
+
and ref.arg_preview
|
|
48
|
+
):
|
|
49
|
+
route = first_string_arg("(" + ref.arg_preview.lstrip("("))
|
|
50
|
+
if route is not None:
|
|
51
|
+
hints.append(
|
|
52
|
+
EntrypointHint(
|
|
53
|
+
rule_id="go.nethttp.route",
|
|
54
|
+
kind=EntrypointKind.HTTP_ROUTE,
|
|
55
|
+
handler_qualified_name=ref.caller_qualified_name,
|
|
56
|
+
route=route,
|
|
57
|
+
http_methods=["*"],
|
|
58
|
+
framework="net/http",
|
|
59
|
+
metadata={"registration": ref.arg_preview},
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
return hints
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _gin_routes(x: FileExtraction) -> list[EntrypointHint]:
|
|
66
|
+
hints = []
|
|
67
|
+
for ref in x.references:
|
|
68
|
+
if (
|
|
69
|
+
ref.kind == "call"
|
|
70
|
+
and ref.receiver_text is not None
|
|
71
|
+
and ref.callee_name in _GIN_METHODS
|
|
72
|
+
and ref.arg_preview
|
|
73
|
+
):
|
|
74
|
+
route = first_string_arg("(" + ref.arg_preview.lstrip("("))
|
|
75
|
+
if route is not None and route.startswith("/"):
|
|
76
|
+
method = ref.callee_name.upper() if ref.callee_name in {
|
|
77
|
+
"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"
|
|
78
|
+
} else "*"
|
|
79
|
+
hints.append(
|
|
80
|
+
EntrypointHint(
|
|
81
|
+
rule_id="go.gin.route",
|
|
82
|
+
kind=EntrypointKind.HTTP_ROUTE,
|
|
83
|
+
handler_qualified_name=ref.caller_qualified_name,
|
|
84
|
+
route=route,
|
|
85
|
+
http_methods=[method],
|
|
86
|
+
framework="gin",
|
|
87
|
+
metadata={"registration": ref.arg_preview},
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
return hints
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _gin_style(framework: str):
|
|
94
|
+
"""chi and fiber share gin's r.Get('/x', handler) registration shape."""
|
|
95
|
+
|
|
96
|
+
def matcher(x: FileExtraction) -> list[EntrypointHint]:
|
|
97
|
+
hints = []
|
|
98
|
+
for hint in _gin_routes(x):
|
|
99
|
+
hints.append(EntrypointHint(
|
|
100
|
+
rule_id=f"go.{framework}.route", kind=hint.kind,
|
|
101
|
+
handler_qualified_name=hint.handler_qualified_name, route=hint.route,
|
|
102
|
+
http_methods=hint.http_methods, framework=framework, metadata=hint.metadata))
|
|
103
|
+
return hints
|
|
104
|
+
|
|
105
|
+
return matcher
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _gorilla_routes(x: FileExtraction) -> list[EntrypointHint]:
|
|
109
|
+
"""gorilla/mux: r.HandleFunc('/x', handler)."""
|
|
110
|
+
hints = []
|
|
111
|
+
for ref in x.references:
|
|
112
|
+
if (
|
|
113
|
+
ref.kind == "call"
|
|
114
|
+
and ref.callee_name in ("HandleFunc", "Handle")
|
|
115
|
+
and ref.receiver_text not in (None, "http")
|
|
116
|
+
and ref.arg_preview
|
|
117
|
+
):
|
|
118
|
+
route = first_string_arg("(" + ref.arg_preview.lstrip("("))
|
|
119
|
+
if route is not None and route.startswith("/"):
|
|
120
|
+
hints.append(EntrypointHint(
|
|
121
|
+
rule_id="go.gorilla-mux.route", kind=EntrypointKind.HTTP_ROUTE,
|
|
122
|
+
handler_qualified_name=ref.caller_qualified_name, route=route,
|
|
123
|
+
http_methods=["*"], framework="gorilla-mux",
|
|
124
|
+
metadata={"registration": ref.arg_preview}))
|
|
125
|
+
return hints
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _cobra_commands(x: FileExtraction) -> list[EntrypointHint]:
|
|
129
|
+
hints = []
|
|
130
|
+
for ref in x.references:
|
|
131
|
+
if ref.kind == "composite" and ref.callee_text == "cobra.Command":
|
|
132
|
+
hints.append(
|
|
133
|
+
EntrypointHint(
|
|
134
|
+
rule_id="go.cobra.command",
|
|
135
|
+
kind=EntrypointKind.CLI_COMMAND,
|
|
136
|
+
handler_qualified_name=ref.caller_qualified_name,
|
|
137
|
+
name=first_string_arg("(" + (ref.arg_preview or "") + ")") or None,
|
|
138
|
+
framework="cobra",
|
|
139
|
+
span=ref.span,
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
return hints
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
register(EntrypointRule("go.core.main", "go", None,
|
|
146
|
+
EntrypointKind.MAIN, _go_main))
|
|
147
|
+
register(EntrypointRule("go.nethttp.route", "go", "net/http",
|
|
148
|
+
EntrypointKind.HTTP_ROUTE, _nethttp_routes))
|
|
149
|
+
register(EntrypointRule("go.gin.route", "go", "gin",
|
|
150
|
+
EntrypointKind.HTTP_ROUTE, _gin_routes))
|
|
151
|
+
register(EntrypointRule("go.chi.route", "go", "chi",
|
|
152
|
+
EntrypointKind.HTTP_ROUTE, _gin_style("chi")))
|
|
153
|
+
register(EntrypointRule("go.fiber.route", "go", "fiber",
|
|
154
|
+
EntrypointKind.HTTP_ROUTE, _gin_style("fiber")))
|
|
155
|
+
register(EntrypointRule("go.gorilla-mux.route", "go", "gorilla-mux",
|
|
156
|
+
EntrypointKind.HTTP_ROUTE, _gorilla_routes))
|
|
157
|
+
register(EntrypointRule("go.cobra.command", "go", "cobra",
|
|
158
|
+
EntrypointKind.CLI_COMMAND, _cobra_commands))
|