entrygraph
Query your codebase like a graph. entrygraph indexes a repository into a
SQLite database (through the SQLAlchemy ORM) and answers questions about
symbols, classes/methods, entrypoints (HTTP routes, CLI commands,
main functions, tasks, lambda handlers), the call graph (callers, callees,
references), and source → sink reachability ("can any HTTP route reach
subprocess.run?").
Language-agnostic via tree-sitter; first-class support for Python, JavaScript/TypeScript, Go, Java, Ruby, C#, PHP, and Rust, with language and framework detection.
Entrypoints include decorator/attribute routes, call-based route registration,
middleware, and config-file handlers (serverless, SAM, Procfile, Dockerfile).
Reachability enumerates real call paths from a catalog taint source to a tagged
sink and reports checkable facts about each — the sink's catalog severity,
the weakest edge confidence, and a same-function reaching-defs verdict (flow: confirmed / not observed) — with class-hierarchy analysis to recover virtual
dispatch. Every hop carries a file:line and the literal source and sink lines,
so a finding is a lead you can open and verify, not a score to trust.
Install
pip install entrygraph # or: uv pip install entrygraph
Requires Python ≥ 3.13. Installs the entrygraph command (you can also run it as
uv run entrygraph … or python -m entrygraph …).
Quick start (CLI)
Index a repo once, then query it as often as you like:
cd /path/to/acme-api
entrygraph index . # build the graph
entrygraph entrypoints # query it — no --db needed
One global store, auto-scoped. By default every index writes into a single
shared database at ~/.entrygraph/.entrygraph.db, keyed by repo root, and every
query command scopes to the repository whose root is your working directory (or
its nearest ancestor). So you index each repo once and just cd between them —
no per-project file to track. To query a repo from outside its directory, pass
--repo <path-or-name> (e.g. --repo acme-api); entrygraph repos lists every
indexed repository and the names you can pass. --db PATH points any command at
an isolated database instead (handy in CI), and every query command takes
--json for machine-readable output.
$ entrygraph repos
┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━┓
┃NAME ┃ ROOT ┃ FILES ┃ SYMBOLS┃
┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━┩
│acme-api │ /Users/you/acme-api │ 42 │ 611 │
│acme-web │ /Users/you/acme-web │ 118 │ 1930 │
└──────────┴───────────────────────┴───────┴────────┘
index — build the graph
Walk the tree and extract symbols, imports, and calls into the index.
Incremental by default (only changed files are reparsed); --full rebuilds,
--paranoid re-hashes every file (skips the mtime fast path), and
--include-tests indexes test files too (excluded by default; flipping it needs
--full).
entrygraph index .
╭───────────────── ✓ indexed acme-api ──────────────────╮
│ files 5 indexed, 0 skipped, 0 deleted of 5 scanned │
│ graph 32 symbols 34 edges 5 entrypoints │
│ db /Users/you/.entrygraph/.entrygraph.db │
╰─────────────────────── 0.137s ────────────────────────╯
The positional argument may also be a git URL — entrygraph clones it and indexes the checkout:
entrygraph index https://github.com/semgrep/semgrep # or git@github.com:org/repo.git
The clone lands in a reused workspace (./.entrygraph/clones/<host>/<org>/<repo>)
and the graph goes into the same global index (keyed by the checkout root), so
follow-up queries run from that checkout directory or with --db. Re-running
index <url> fetches and updates the existing checkout instead of re-cloning.
The clone is hardened — shallow, repo hooks disabled, no interactive credential
prompt, and a wall-clock timeout — and the indexed code is never executed.
| URL flag | Meaning |
|---|---|
--ref REF |
branch, tag, or commit to check out (default: remote HEAD) |
--depth N / --full-clone |
clone depth (default 1; --full-clone = full history) |
--clone-dir DIR |
where to place the checkout |
--ephemeral |
clone to a temp dir and delete it after indexing (no paths snippets afterward) |
--timeout SECONDS |
max clone/fetch wall-time (default 600) |
Private repos work when the ambient git environment already authenticates (SSH agent, credential helper, or a token in the URL); entrygraph never prompts for or stores secrets.
detect — languages & frameworks
Byte-share per language plus framework detections scored from manifest dependencies and code signals.
entrygraph detect
Languages
┏━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓
┃LANGUAGE ┃ FILES ┃ SHARE ┃
┡━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩
│python │ 5 │ ████████████ 100.0%│
└─────────┴───────┴────────────────────┘
Frameworks
┏━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓
┃FRAMEWORK ┃ LANGUAGE ┃ CONFIDENCE ┃
┡━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━┩
│flask │ python │ █████████░ 0.94│
│click │ python │ █████████░ 0.94│
└──────────┴──────────┴────────────────┘
entrypoints — your attack surface
Every HTTP route, CLI command, task, lambda, middleware, and main — with its
framework, method, route, and handler symbol, grouped by kind, framework, and
route. Filter with --kind, --framework, or --route, and cap with --limit.
entrypoints --kind http_route # or: --framework flask / --route '/api/*'
┏━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃KIND ┃ FRAMEWORK ┃ METHOD ┃ ROUTE ┃ HANDLER ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━┩
│http_route │ flask │ GET │ /users/<user_id> │ app.routes.get_user │
│http_route │ flask │ GET │ /health │ app.routes.health │
│http_route │ flask │ GET,POST │ /reports │ app.routes.create_report│
│cli_command │ click │ │ │ cli.report │
│main │ │ │ │ cli │
└────────────┴───────────┴──────────┴──────────────────┴─────────────────────────┘
5 entrypoint(s)
symbols, callers, callees, references — search & walk the call graph
symbols globs on --name or --qname (filter by --kind/--file, cap with
--limit):
entrygraph symbols --kind class --name 'Report*'
┏━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━┓
┃KIND ┃ QNAME ┃ FILE ┃ LINE┃
┡━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━┩
│class │ app.services.ReportRunner │ app/services.py │ 10│
└──────┴───────────────────────────┴─────────────────┴─────┘
callers/callees walk the call graph (--depth N, default 1) and list the
distinct symbols on the other end of an edge:
entrygraph callers app.services.run_report # who calls it
entrygraph callees app.services.run_report # what it calls
┏━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃KIND ┃ QNAME ┃ FILE ┃
┡━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
│function │ app.routes.create_report │ app/routes.py│
│function │ cli.report │ cli.py │
└─────────┴──────────────────────────┴──────────────┘
By default callers/callees list only resolved edges (exact/import and
unique-name fuzzy binds). --include-speculative adds class-hierarchy guesses
and unresolved wildcard/dynamic calls (lower confidence, noisier).
references is the drill-down: instead of distinct caller symbols, it lists
every individual call site targeting a symbol, each with its file:line and
edge-resolution confidence — the checkable form you act on:
entrygraph references app.services.run_report
┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
┃CALLER ┃ LOCATION ┃ CONFIDENCE┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
│app.routes.create_report │ app/routes.py:20 │ import │
│cli.report │ cli.py:11 │ import │
└─────────────────────────┴──────────────────┴───────────┘
paths — source → sink reachability
Can anything reach a dangerous sink? Paths are drawn as call cards, ordered by
their facts — confirmed flows first, then by sink severity, then by the weakest
edge confidence. Each hop shows its resolution confidence
(exact/import/fuzzy/unresolved) and its file:line; the sink node is
flagged (⚡), and the literal source and sink lines are printed so you can read
the actual call.
entrygraph paths --source-category http_input --sink-category command_exec
1 path(s) category:http_input → category:command_exec
[1] severity high confidence import
source create_report app/routes.py:12 (http_input · explicit · query "cmd")
cmd = request.args.get("cmd")
↓ run_report app/routes.py:20 import
↓ start app/services.py:27 fuzzy
sink subprocess.run app/services.py:22 ⚡ py.command-exec.subprocess import
subprocess.run(cmd, shell=True)
flow: confirmed (2 hops)
-
Reachability check:
pathsexits0when a path is found,1when none —entrygraph paths --source-category http_input --sink-category command_exec && echo reachable. -
Sources & sinks: use
--source-category/--sink-categoryto start from every registered taint source and end at every tagged sink of a category, or name an exact--source/--sink(the language prefix is optional —--sink subprocess.runresolves topy:subprocess.run). Combine a--sourceglob with a category to union both.paths --list-categoriesprints the valid category names for the index; an unknown category is a hard error, never a silent empty result:entrygraph paths --list-categories # source categories cli_arg, env_input, http_input, stdin_input, user_input # sink categories code_eval, command_exec, deserialization, path_traversal, sql, ssrf, …
-
Precision/recall dial: by default the search is adaptive — it tries only high-confidence (resolved) edges first and automatically widens to the speculative frontier if that finds nothing.
--strictdisables the widening (resolved edges only). To force a specific frontier for one run: widen with--include-unresolved(wildcardpy:*.executesinks + dynamic calls),--include-fuzzy(speculative class-hierarchy edges), or--include-callbacks(function/method values passed as arguments — handler registrations likehttp.HandleFunc("/", handler)orthis::handle);--min-confidence Nsets an explicit floor. Bound the search with--max-depth(default 25) and--max-paths(default 10). -
Source provenance: an
http_input/cli_argsource is labeled· explicitwhen the handler demonstrably reads request input (a catalog accessor call likerequest.args.get("q")) or· handlerwhen the handler is merely shaped like a source and reaches the sink without a proven read.--explicit-sourcesdrops the handler-only seeds entirely (at the cost of property-read frameworks like Expressreq.body). -
Flow verification: a bounded reaching-defs check labels each path with whether a source value actually flows to the sink —
flow: confirmedwhen it does,flow: not observedwhen it provably doesn't. It follows up to--taint-hopsinterior call hops (default 5;0= same-function only) and is conservative: anything it can't analyze stays unlabeled.--confirmed-onlykeeps just the confirmed paths.
What a finding means. A path is a reachability lead to triage, not a
confirmed dataflow: it says a source-bearing symbol can reach a sink-bearing
symbol through the call graph. Every field is a checkable fact — open the
file:lines and read the code. The severity is the tagged sink's catalog
severity; the per-hop confidence tags (exact/fuzzy/unresolved) are
edge-resolution confidence, not taint confidence; the flow: label is the
reaching-defs verdict where the check can see the code. There is no blended
"risk" number to trust — the ordering surfaces confirmed, higher-severity,
better-resolved paths first so you triage the list top-down.
stats & --json
entrygraph stats
entrygraph --help # every command and flag
╭──────────────── index stats ────────────────╮
│ ┏━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓ │
│ ┃metric ┃ value┃ │
│ ┡━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩ │
│ │repo_root │ /path/to/acme-api │ │
│ │index_generation │ 1│ │
│ │files │ 5│ │
│ │symbols │ 32│ │
│ │edges │ 34│ │
│ │resolved_edges │ 34│ │
│ │entrypoints │ 5│ │
│ │sink_edges │ 2│ │
│ │source_edges │ 1│ │
│ └─────────────────┴──────────────────────┘ │
╰──────────────────────────────────────────────╯
taint catalog: python full
Add --json to any query command for machine-readable output. Each path carries
its severity, min_confidence (the weakest edge confidence, 0 unresolved →
3 exact), taint_verified (the flow verdict), source provenance, the symbol
chain, and the literal source/sink lines:
[
{
"length": 5,
"min_confidence": 2,
"severity": "high",
"may_continue": false,
"source_kind": "explicit",
"taint_verified": true,
"source_channel": "query",
"source_key": "name",
"symbols": [
"app.routes.create_report", "app.services.run_report",
"app.services.ReportRunner.start",
"app.services.ReportRunner.render_and_execute", "py:subprocess.run"
],
"lines": [20, 27, 17, 22],
"source_line": "cmd = request.args.get(\"cmd\")",
"sink_line": "subprocess.run(cmd, shell=True)"
}
]
Colored tables, share/confidence bars, and severity highlighting render in a real terminal; piped or
--jsonoutput is plain text.
serve — a web UI over the index
To browse an index visually rather than via the CLI, entrygraph serve runs a
web app that walks a repo's symbols, entrypoints/routes,
callers/callees, source→sink paths, and an interactive call graph,
and can register and index repositories from the UI:
entrygraph index . --db /tmp/graph.db
entrygraph serve --db /tmp/graph.db # http://127.0.0.1:8100
It supports Authentik SSO (EG_OIDC_*), with a zero-setup local mode (no auth)
on loopback by default, and ships behind the entrygraph[server] extra. Build
the UI once with cd webapp && npm run build; the build lands in the package and
is served at /.
Python API
from entrygraph import CodeGraph
# Index a repo (creates <repo>/.entrygraph.db by default)
graph = CodeGraph.index("/path/to/repo")
# ...or open an existing index
graph = CodeGraph.open("graph.db")
# Symbols — glob on name or qualified name, filter by kind or file
graph.symbols(kind="class", name="User*")
graph.symbol("app.services.Runner.execute") # exact; raises if missing
# Detection
report = graph.detect()
report.languages # -> [DetectedLanguage(name="python", percent=96.7, ...), ...]
report.frameworks # -> [DetectedFramework(name="flask", confidence=0.94, ...), ...]
# Entrypoints
graph.entrypoints(framework="flask")
graph.entrypoints(kind="http_route", route="/api/*")
# Call graph
graph.callers("app.services.run_report") # who calls it
graph.callees("app.services.run_report", depth=3) # what it (transitively) calls
graph.references("app.models.CONST") # inbound edges of any kind
# Source -> sink reachability (ordered by facts: confirmed flows, severity, confidence)
paths = graph.paths(source="app.routes.*", sink_category="command_exec")
for p in paths:
print(p.severity, p.taint_verified, p.render(), "(+may continue)" if p.may_continue else "")
# high True app.routes.create_report -> app.services.run_report (line 20)
# -> ...ReportRunner.render_and_execute (line 17) -> py:subprocess.run (line 22)
graph.reachable(source="app.routes.upload", sink="py:subprocess.run") # -> bool
# Valid category names (an unknown category raises UnknownCategoryError)
graph.sink_categories() # -> ["command_exec", "sql", "path_traversal", ...]
graph.source_categories() # -> ["http_input", "cli_arg", "env_input", ...]
# Precision/recall dial. By default only EXACT/IMPORT and unique-name FUZZY
# edges are traversed. Opt into wider (noisier) traversal:
graph.paths(source="app.routes.*", sink_category="sql",
include_unresolved=True) # follow py:*.execute wildcard-sink guesses
graph.paths(source="app.routes.*", sink_category="command_exec",
include_fuzzy=True) # follow speculative class-hierarchy (CHA) edges
graph.paths(source="app.routes.*", sink_category="command_exec",
confirmed_only=True) # keep only paths where a flow is confirmed
# Incremental re-index (only changed/added/deleted files are reparsed)
graph.refresh()
# Escape hatches
graph.session() # raw SQLAlchemy Session
graph.sql("SELECT ...") # textual query -> list[dict]
Every result is a frozen, immutable dataclass detached from the DB session, so results are safe to hold and trivial to serialize.
How it works
- Walk —
os.scandirwith hard-pruned junk dirs (node_modules,.venv, …),.gitignorerules, and size/binary/minified gates. Every skip is recorded with a reason. - Extract — tree-sitter
.scmqueries harvest definitions/imports/calls; small per-language "shaper" modules build qualified names, import maps, and receiver info. Parsing runs across a process pool for large repos. - Resolve — a two-pass resolver binds references to symbols with a
confidence level (
exact/import/fuzzy/unresolved). External callees (subprocess.run,child_process.exec, …) become placeholder nodes so sinks are real graph terminals. - Detect — frameworks are scored from manifest dependencies plus code signals (noisy-or); entrypoint rules map framework patterns to route/command records.
- Store — everything persists to SQLite via the SQLAlchemy 2.0 ORM with bulk inserts and app-assigned keys. Re-indexing is incremental and content-hash driven.
- Query — reachability runs over an in-memory adjacency cache (BFS/DFS with
cycle handling); a recursive-CTE SQL engine is available as a fallback
(
engine="sql").
Extending
- Custom sinks/sources — drop an
entrygraph.tomlin the repo root with[[sink]]/[[source]]tables (same schema as the built-indata/sinks/*.toml), or callentrygraph.detect.taint.register_sink(...)/register_source(...). Third-party wrapper libraries that reach a sink internally are covered bydata/sinks/lib_*.toml"library summaries" (same schema, with alibrary = "..."tag). - New frameworks / entrypoints — register a
FrameworkSpecand anEntrypointRule; adding a framework is usually a few lines. - New languages — add a
<lang>/{definitions,imports,calls}.scmquery set and a shaper implementing theLanguageExtractorprotocol.
Releasing
Merging to main auto-bumps the patch version (via a git tag) and publishes to
PyPI through Trusted Publishing — see RELEASING.md. The package
version is derived from git tags by hatch-vcs, so it's never hand-edited.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file entrygraph-0.1.123.tar.gz.
File metadata
- Download URL: entrygraph-0.1.123.tar.gz
- Upload date:
- Size: 437.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ec16c4710d95d9a8fd027fd7ee8bcd92173b6a6a3e3943deea9cefbea8de836b
|
|
| MD5 |
6d896f6eda32dccda4423918443bf0ef
|
|
| BLAKE2b-256 |
3c2fe72323708db53f4fa078b9a41b041df60e20fac6170588032c1cc744c362
|
Provenance
The following attestation bundles were made for entrygraph-0.1.123.tar.gz:
Publisher:
release.yml on brettbergin/entrygraph
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
entrygraph-0.1.123.tar.gz -
Subject digest:
ec16c4710d95d9a8fd027fd7ee8bcd92173b6a6a3e3943deea9cefbea8de836b - Sigstore transparency entry: 2187246038
- Sigstore integration time:
-
Permalink:
brettbergin/entrygraph@378492b58295321663615109dcb38bc58db875f8 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/brettbergin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@378492b58295321663615109dcb38bc58db875f8 -
Trigger Event:
push
-
Statement type:
File details
Details for the file entrygraph-0.1.123-py3-none-any.whl.
File metadata
- Download URL: entrygraph-0.1.123-py3-none-any.whl
- Upload date:
- Size: 263.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e0be33df35706feecacca7f33f98d076fad882fc7a7360096f2e7148cd5e8eef
|
|
| MD5 |
f482aa24dcf83b25bd7d060cb7e8e711
|
|
| BLAKE2b-256 |
1a9c77a68ba53df74ff3d295d07465ae916f6f95bc480f89fae2af33c93653ab
|
Provenance
The following attestation bundles were made for entrygraph-0.1.123-py3-none-any.whl:
Publisher:
release.yml on brettbergin/entrygraph
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
entrygraph-0.1.123-py3-none-any.whl -
Subject digest:
e0be33df35706feecacca7f33f98d076fad882fc7a7360096f2e7148cd5e8eef - Sigstore transparency entry: 2187246064
- Sigstore integration time:
-
Permalink:
brettbergin/entrygraph@378492b58295321663615109dcb38bc58db875f8 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/brettbergin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@378492b58295321663615109dcb38bc58db875f8 -
Trigger Event:
push
-
Statement type: