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.
Files changed (102) hide show
  1. entrygraph/__init__.py +87 -0
  2. entrygraph/__main__.py +8 -0
  3. entrygraph/_version.py +24 -0
  4. entrygraph/api.py +549 -0
  5. entrygraph/cli/__init__.py +0 -0
  6. entrygraph/cli/main.py +387 -0
  7. entrygraph/cli/render.py +136 -0
  8. entrygraph/data/sinks/csharp.toml +106 -0
  9. entrygraph/data/sinks/go.toml +87 -0
  10. entrygraph/data/sinks/java.toml +92 -0
  11. entrygraph/data/sinks/javascript.toml +112 -0
  12. entrygraph/data/sinks/lib_javascript.toml +34 -0
  13. entrygraph/data/sinks/lib_python.toml +39 -0
  14. entrygraph/data/sinks/php.toml +125 -0
  15. entrygraph/data/sinks/python.toml +160 -0
  16. entrygraph/data/sinks/ruby.toml +102 -0
  17. entrygraph/data/sinks/rust.toml +68 -0
  18. entrygraph/db/__init__.py +0 -0
  19. entrygraph/db/engine.py +34 -0
  20. entrygraph/db/meta.py +70 -0
  21. entrygraph/db/models.py +176 -0
  22. entrygraph/db/queries.py +155 -0
  23. entrygraph/detect/__init__.py +0 -0
  24. entrygraph/detect/entrypoints/__init__.py +22 -0
  25. entrygraph/detect/entrypoints/base.py +124 -0
  26. entrygraph/detect/entrypoints/configs.py +139 -0
  27. entrygraph/detect/entrypoints/csharp.py +156 -0
  28. entrygraph/detect/entrypoints/golang.py +158 -0
  29. entrygraph/detect/entrypoints/java.py +187 -0
  30. entrygraph/detect/entrypoints/javascript.py +211 -0
  31. entrygraph/detect/entrypoints/php.py +133 -0
  32. entrygraph/detect/entrypoints/python.py +335 -0
  33. entrygraph/detect/entrypoints/ruby.py +147 -0
  34. entrygraph/detect/entrypoints/rust.py +153 -0
  35. entrygraph/detect/frameworks.py +369 -0
  36. entrygraph/detect/manifests.py +234 -0
  37. entrygraph/detect/taint.py +224 -0
  38. entrygraph/errors.py +27 -0
  39. entrygraph/extract/__init__.py +0 -0
  40. entrygraph/extract/base.py +51 -0
  41. entrygraph/extract/csharp.py +502 -0
  42. entrygraph/extract/golang.py +342 -0
  43. entrygraph/extract/ir.py +105 -0
  44. entrygraph/extract/java.py +329 -0
  45. entrygraph/extract/javascript.py +400 -0
  46. entrygraph/extract/php.py +426 -0
  47. entrygraph/extract/python.py +390 -0
  48. entrygraph/extract/registry.py +43 -0
  49. entrygraph/extract/ruby.py +321 -0
  50. entrygraph/extract/rust.py +482 -0
  51. entrygraph/fs/__init__.py +0 -0
  52. entrygraph/fs/hashing.py +78 -0
  53. entrygraph/fs/lang.py +134 -0
  54. entrygraph/fs/walker.py +167 -0
  55. entrygraph/graph/__init__.py +0 -0
  56. entrygraph/graph/adjacency.py +146 -0
  57. entrygraph/graph/cte.py +123 -0
  58. entrygraph/graph/scoring.py +101 -0
  59. entrygraph/kinds.py +51 -0
  60. entrygraph/parsing/__init__.py +0 -0
  61. entrygraph/parsing/parsers.py +49 -0
  62. entrygraph/parsing/queries.py +39 -0
  63. entrygraph/pipeline/__init__.py +0 -0
  64. entrygraph/pipeline/scanner.py +506 -0
  65. entrygraph/pipeline/worker.py +49 -0
  66. entrygraph/pipeline/writer.py +41 -0
  67. entrygraph/py.typed +0 -0
  68. entrygraph/queries/csharp/calls.scm +4 -0
  69. entrygraph/queries/csharp/definitions.scm +29 -0
  70. entrygraph/queries/csharp/imports.scm +4 -0
  71. entrygraph/queries/go/calls.scm +2 -0
  72. entrygraph/queries/go/definitions.scm +24 -0
  73. entrygraph/queries/go/imports.scm +1 -0
  74. entrygraph/queries/java/calls.scm +2 -0
  75. entrygraph/queries/java/definitions.scm +14 -0
  76. entrygraph/queries/java/imports.scm +2 -0
  77. entrygraph/queries/javascript/calls.scm +4 -0
  78. entrygraph/queries/javascript/definitions.scm +4 -0
  79. entrygraph/queries/javascript/imports.scm +6 -0
  80. entrygraph/queries/php/calls.scm +8 -0
  81. entrygraph/queries/php/definitions.scm +24 -0
  82. entrygraph/queries/php/imports.scm +1 -0
  83. entrygraph/queries/python/calls.scm +2 -0
  84. entrygraph/queries/python/definitions.scm +11 -0
  85. entrygraph/queries/python/imports.scm +2 -0
  86. entrygraph/queries/ruby/calls.scm +4 -0
  87. entrygraph/queries/ruby/definitions.scm +20 -0
  88. entrygraph/queries/ruby/imports.scm +7 -0
  89. entrygraph/queries/rust/calls.scm +5 -0
  90. entrygraph/queries/rust/definitions.scm +26 -0
  91. entrygraph/queries/rust/imports.scm +4 -0
  92. entrygraph/resolve/__init__.py +0 -0
  93. entrygraph/resolve/externals.py +61 -0
  94. entrygraph/resolve/hierarchy.py +152 -0
  95. entrygraph/resolve/resolver.py +275 -0
  96. entrygraph/resolve/symbol_table.py +48 -0
  97. entrygraph/results.py +138 -0
  98. entrygraph-0.1.0.dist-info/METADATA +204 -0
  99. entrygraph-0.1.0.dist-info/RECORD +102 -0
  100. entrygraph-0.1.0.dist-info/WHEEL +4 -0
  101. entrygraph-0.1.0.dist-info/entry_points.txt +2 -0
  102. 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] + "…"