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,224 @@
|
|
|
1
|
+
"""Source/sink/sanitizer pattern registry.
|
|
2
|
+
|
|
3
|
+
Built-in catalogs ship as TOML package data — every ``entrygraph/data/sinks/
|
|
4
|
+
*.toml`` file is loaded (per-language ``<lang>.toml`` plus ``lib_*.toml``
|
|
5
|
+
third-party wrapper summaries) — in the same schema users write in a repo-root
|
|
6
|
+
``entrygraph.toml``. Patterns are language-prefixed qualified-name globs with
|
|
7
|
+
brace expansion, e.g. ``py:subprocess.{run,call,Popen}``; they are compiled to
|
|
8
|
+
one alternation regex per registry and matched against each call edge's
|
|
9
|
+
canonical callee (dst_qname) at index time, stamping ``edges.sink_id``.
|
|
10
|
+
|
|
11
|
+
Sanitizers are matched at query time (not index time) so retuning them never
|
|
12
|
+
requires a re-index: a path passing through a registered sanitizer for the
|
|
13
|
+
sink's category is downgraded (``effect="reduces"``) or pruned
|
|
14
|
+
(``effect="neutralizes"``).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import fnmatch
|
|
20
|
+
import re
|
|
21
|
+
import tomllib
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from functools import cache
|
|
24
|
+
from importlib.resources import files as resource_files
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, slots=True)
|
|
29
|
+
class SinkPattern:
|
|
30
|
+
id: str
|
|
31
|
+
category: str
|
|
32
|
+
callee: str # brace-glob over canonical qnames
|
|
33
|
+
severity: str = "medium"
|
|
34
|
+
description: str = ""
|
|
35
|
+
require_arg_hint: str | None = None # regex against arg_preview to cut noise
|
|
36
|
+
library: str | None = None # third-party wrapper this summarizes (informational)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class SourcePattern:
|
|
41
|
+
id: str
|
|
42
|
+
category: str
|
|
43
|
+
callee: str
|
|
44
|
+
description: str = ""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True, slots=True)
|
|
48
|
+
class SanitizerPattern:
|
|
49
|
+
"""A call that neutralizes or reduces taint for a sink category.
|
|
50
|
+
|
|
51
|
+
`effect="neutralizes"` means the value is considered safe downstream (paths
|
|
52
|
+
through it can be pruned); `effect="reduces"` only discounts the risk score.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
id: str
|
|
56
|
+
category: str
|
|
57
|
+
callee: str # brace-glob over canonical qnames
|
|
58
|
+
effect: str = "reduces" # "neutralizes" | "reduces"
|
|
59
|
+
description: str = ""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def expand_braces(pattern: str) -> list[str]:
|
|
63
|
+
match = re.search(r"\{([^{}]*)\}", pattern)
|
|
64
|
+
if not match:
|
|
65
|
+
return [pattern]
|
|
66
|
+
head, tail = pattern[: match.start()], pattern[match.end():]
|
|
67
|
+
expanded: list[str] = []
|
|
68
|
+
for option in match.group(1).split(","):
|
|
69
|
+
expanded.extend(expand_braces(head + option.strip() + tail))
|
|
70
|
+
return expanded
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _compile(callee: str) -> re.Pattern:
|
|
74
|
+
return re.compile("|".join(fnmatch.translate(g) for g in expand_braces(callee)))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class SinkRegistry:
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
sinks: list[SinkPattern],
|
|
81
|
+
sources: list[SourcePattern],
|
|
82
|
+
sanitizers: list[SanitizerPattern] | None = None,
|
|
83
|
+
) -> None:
|
|
84
|
+
self.sinks = {s.id: s for s in sinks}
|
|
85
|
+
self.sources = {s.id: s for s in sources}
|
|
86
|
+
self.sanitizers = {s.id: s for s in (sanitizers or [])}
|
|
87
|
+
self._compiled = [(_compile(s.callee), s) for s in sinks]
|
|
88
|
+
self._compiled_sanitizers = [(_compile(s.callee), s) for s in (sanitizers or [])]
|
|
89
|
+
|
|
90
|
+
def match(self, canonical_callee: str, arg_preview: str | None = None) -> str | None:
|
|
91
|
+
"""Return the first matching sink id, or None."""
|
|
92
|
+
for regex, sink in self._compiled:
|
|
93
|
+
if regex.match(canonical_callee):
|
|
94
|
+
if sink.require_arg_hint and not (
|
|
95
|
+
arg_preview and re.search(sink.require_arg_hint, arg_preview)
|
|
96
|
+
):
|
|
97
|
+
continue
|
|
98
|
+
return sink.id
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
def match_sanitizers(self, canonical_callee: str) -> list[SanitizerPattern]:
|
|
102
|
+
"""Sanitizers whose pattern matches this callee qname."""
|
|
103
|
+
return [s for regex, s in self._compiled_sanitizers if regex.match(canonical_callee)]
|
|
104
|
+
|
|
105
|
+
def sanitizers_for_category(self, category: str) -> list[SanitizerPattern]:
|
|
106
|
+
return [s for s in self.sanitizers.values() if s.category == category]
|
|
107
|
+
|
|
108
|
+
def ids_for_category(self, category: str) -> set[str]:
|
|
109
|
+
return {s.id for s in self.sinks.values() if s.category == category}
|
|
110
|
+
|
|
111
|
+
def severity_of(self, sink_id: str | None) -> str | None:
|
|
112
|
+
sink = self.sinks.get(sink_id) if sink_id else None
|
|
113
|
+
return sink.severity if sink else None
|
|
114
|
+
|
|
115
|
+
def merged_with(
|
|
116
|
+
self,
|
|
117
|
+
sinks: list[SinkPattern],
|
|
118
|
+
sources: list[SourcePattern],
|
|
119
|
+
disable: list[str] | None = None,
|
|
120
|
+
sanitizers: list[SanitizerPattern] | None = None,
|
|
121
|
+
) -> "SinkRegistry":
|
|
122
|
+
disabled = set(disable or [])
|
|
123
|
+
kept = [s for s in self.sinks.values() if s.id not in disabled]
|
|
124
|
+
kept_san = [s for s in self.sanitizers.values() if s.id not in disabled]
|
|
125
|
+
return SinkRegistry(
|
|
126
|
+
[*kept, *sinks],
|
|
127
|
+
[*self.sources.values(), *sources],
|
|
128
|
+
[*kept_san, *(sanitizers or [])],
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _load_toml(
|
|
133
|
+
text: str,
|
|
134
|
+
) -> tuple[list[SinkPattern], list[SourcePattern], list[SanitizerPattern], list[str]]:
|
|
135
|
+
data = tomllib.loads(text)
|
|
136
|
+
sinks = [
|
|
137
|
+
SinkPattern(
|
|
138
|
+
id=raw["id"],
|
|
139
|
+
category=raw["category"],
|
|
140
|
+
callee=raw["callee"],
|
|
141
|
+
severity=raw.get("severity", "medium"),
|
|
142
|
+
description=raw.get("description", ""),
|
|
143
|
+
require_arg_hint=raw.get("require_arg_hint"),
|
|
144
|
+
library=raw.get("library"),
|
|
145
|
+
)
|
|
146
|
+
for raw in data.get("sink", [])
|
|
147
|
+
]
|
|
148
|
+
sources = [
|
|
149
|
+
SourcePattern(
|
|
150
|
+
id=raw["id"],
|
|
151
|
+
category=raw["category"],
|
|
152
|
+
callee=raw["callee"],
|
|
153
|
+
description=raw.get("description", ""),
|
|
154
|
+
)
|
|
155
|
+
for raw in data.get("source", [])
|
|
156
|
+
]
|
|
157
|
+
sanitizers = [
|
|
158
|
+
SanitizerPattern(
|
|
159
|
+
id=raw["id"],
|
|
160
|
+
category=raw["category"],
|
|
161
|
+
callee=raw["callee"],
|
|
162
|
+
effect=raw.get("effect", "reduces"),
|
|
163
|
+
description=raw.get("description", ""),
|
|
164
|
+
)
|
|
165
|
+
for raw in data.get("sanitizer", [])
|
|
166
|
+
]
|
|
167
|
+
return sinks, sources, sanitizers, list(data.get("disable", []))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@cache
|
|
171
|
+
def builtin_registry() -> SinkRegistry:
|
|
172
|
+
"""All shipped catalogs: data/sinks/*.toml (per-language + lib_* summaries)."""
|
|
173
|
+
sinks: list[SinkPattern] = []
|
|
174
|
+
sources: list[SourcePattern] = []
|
|
175
|
+
sanitizers: list[SanitizerPattern] = []
|
|
176
|
+
data_dir = resource_files("entrygraph") / "data" / "sinks"
|
|
177
|
+
for entry in sorted(data_dir.iterdir(), key=lambda p: p.name):
|
|
178
|
+
if not entry.name.endswith(".toml"):
|
|
179
|
+
continue
|
|
180
|
+
s, src, san, _ = _load_toml(entry.read_text())
|
|
181
|
+
sinks.extend(s)
|
|
182
|
+
sources.extend(src)
|
|
183
|
+
sanitizers.extend(san)
|
|
184
|
+
return SinkRegistry(sinks, sources, sanitizers)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
_user_sinks: list[SinkPattern] = []
|
|
188
|
+
_user_sources: list[SourcePattern] = []
|
|
189
|
+
_user_sanitizers: list[SanitizerPattern] = []
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def register_sink(sink: SinkPattern) -> None:
|
|
193
|
+
"""Library-embedder extension point; applies to subsequent index runs."""
|
|
194
|
+
_user_sinks.append(sink)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def register_source(source: SourcePattern) -> None:
|
|
198
|
+
_user_sources.append(source)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def register_sanitizer(sanitizer: SanitizerPattern) -> None:
|
|
202
|
+
_user_sanitizers.append(sanitizer)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def registry_for_repo(root: str | Path | None = None) -> SinkRegistry:
|
|
206
|
+
"""Built-ins + Python-registered patterns + repo-root entrygraph.toml."""
|
|
207
|
+
registry = builtin_registry()
|
|
208
|
+
extra_sinks = list(_user_sinks)
|
|
209
|
+
extra_sources = list(_user_sources)
|
|
210
|
+
extra_sanitizers = list(_user_sanitizers)
|
|
211
|
+
disable: list[str] = []
|
|
212
|
+
if root is not None:
|
|
213
|
+
config = Path(root) / "entrygraph.toml"
|
|
214
|
+
if config.is_file():
|
|
215
|
+
try:
|
|
216
|
+
file_sinks, file_sources, file_san, disable = _load_toml(config.read_text())
|
|
217
|
+
extra_sinks.extend(file_sinks)
|
|
218
|
+
extra_sources.extend(file_sources)
|
|
219
|
+
extra_sanitizers.extend(file_san)
|
|
220
|
+
except (tomllib.TOMLDecodeError, KeyError):
|
|
221
|
+
pass
|
|
222
|
+
if extra_sinks or extra_sources or extra_sanitizers or disable:
|
|
223
|
+
return registry.merged_with(extra_sinks, extra_sources, disable, extra_sanitizers)
|
|
224
|
+
return registry
|
entrygraph/errors.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Exception hierarchy for entrygraph."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class EntrygraphError(Exception):
|
|
7
|
+
"""Base class for all entrygraph errors."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SchemaMismatchError(EntrygraphError):
|
|
11
|
+
"""The database was written by an incompatible entrygraph schema version."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DatabaseNotFoundError(EntrygraphError):
|
|
15
|
+
"""No index database exists at the given path."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SymbolNotFoundError(EntrygraphError):
|
|
19
|
+
"""No symbol matches the given qualified name."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RepositoryNotIndexedError(EntrygraphError):
|
|
23
|
+
"""The database contains no indexed repository."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class UnsupportedLanguageError(EntrygraphError):
|
|
27
|
+
"""No extractor is registered for the requested language."""
|
|
File without changes
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Extractor protocol and shared tree-walking helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING, ClassVar, Protocol
|
|
7
|
+
|
|
8
|
+
from entrygraph.extract.ir import FileExtraction, Span
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
11
|
+
from tree_sitter import Node, Tree
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(slots=True)
|
|
15
|
+
class FileContext:
|
|
16
|
+
path: str # repo-relative
|
|
17
|
+
language: str
|
|
18
|
+
module_path: str
|
|
19
|
+
source: bytes
|
|
20
|
+
is_package: bool = False # e.g. Python __init__.py
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LanguageExtractor(Protocol):
|
|
24
|
+
language_ids: ClassVar[tuple[str, ...]]
|
|
25
|
+
|
|
26
|
+
def module_path_for(self, repo_relative_path: str) -> tuple[str, bool]:
|
|
27
|
+
"""Return (module_path, is_package) for a repo-relative path."""
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
def extract(self, tree: "Tree", ctx: FileContext) -> FileExtraction:
|
|
31
|
+
"""Run queries + shaping. Must not raise on partial trees and must
|
|
32
|
+
return plain-data IR only (no tree-sitter objects)."""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def node_text(node: "Node") -> str:
|
|
37
|
+
return (node.text or b"").decode("utf-8", errors="replace")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def span_of(node: "Node") -> Span:
|
|
41
|
+
return Span(
|
|
42
|
+
start_line=node.start_point.row + 1,
|
|
43
|
+
start_col=node.start_point.column,
|
|
44
|
+
end_line=node.end_point.row + 1,
|
|
45
|
+
end_col=node.end_point.column,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def truncate(text: str, limit: int = 80) -> str:
|
|
50
|
+
text = " ".join(text.split())
|
|
51
|
+
return text if len(text) <= limit else text[: limit - 1] + "…"
|