Language-agnostic code graph: query symbols, entrypoints, and source-to-sink call paths from a SQLite index
Project description
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), and source → sink call-graph
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.
Reachability is a heuristic taint tier, not just call-edge closure: paths are risk-ranked, sanitizers prune or discount them, class-hierarchy analysis recovers virtual dispatch, and confidence flags trade recall for precision. Entrypoints include decorator/attribute routes, call-based route registration, middleware, and config-file handlers (serverless, SAM, Procfile, Dockerfile).
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 the resulting .entrygraph.db as often as you
like. Every query command takes --db PATH (defaults to discovering
.entrygraph.db) and --json for machine-readable output.
index — build the graph
Walk the tree and extract symbols, imports, and calls into .entrygraph.db.
Incremental by default (only changed files are reparsed); --full rebuilds.
entrygraph index .
╭───────────────── ✓ indexed acme-api ──────────────────╮
│ files 5 indexed, 0 skipped, 0 deleted of 5 scanned │
│ graph 32 symbols 34 edges 5 entrypoints │
│ db /path/to/acme-api/.entrygraph.db │
╰─────────────────────── 0.137s ────────────────────────╯
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. Filter with --kind,
--framework, or --route.
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 — search & walk the call graph
symbols globs on name or qualified name (filter by --kind/--file);
callers/callees walk the call graph (--depth N).
entrygraph symbols --kind class --name 'Report*'
entrygraph callers app.services.run_report # who calls it
entrygraph callees app.services.run_report # what it calls
┏━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━┓
┃KIND ┃ QNAME ┃ FILE ┃ LINE┃
┡━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━┩
│class │ app.services.ReportRunner │ app/services.py │ 10│
└──────┴───────────────────────────┴─────────────────┴─────┘
paths — source → sink reachability
The security workhorse: can anything reach a dangerous sink? Paths are
risk-ranked (highest first) and drawn as call trees — the sink node is
flagged (⚑), each hop shows its resolution confidence
(exact/import/fuzzy/unresolved), and badges mark constant-argument sinks
and speculative edges.
entrygraph paths --source '*' --sink-category command_exec
7 path(s) * → category:command_exec
[1] ■ risk 0.73 app.services.ReportRunner.render_and_execute
└── → py:subprocess.run line 22 import ⚑ py.command-exec.subprocess
[4] ■ risk 0.50 app.services.run_report
└── → app.services.ReportRunner.start line 27 fuzzy
└── → app.services.ReportRunner.render_and_execute line 17 exact
└── → py:subprocess.run line 22 import ⚑ py.command-exec.subprocess
[5] ■ risk 0.49 app.routes.create_report
└── → app.services.run_report line 20 import
└── → app.services.ReportRunner.start line 27 fuzzy
└── → app.services.ReportRunner.render_and_execute line 17 exact
└── → py:subprocess.run line 22 import ⚑ py.command-exec.subprocess
- CI gate: exits
0when a path is found,1when none —entrygraph paths --source '*' --sink-category command_exec && echo reachable. - Catalog sources: instead of a
--sourceglob, use--source-categoryto start from every call site of a registered taint source (e.g.--source-category http_input/env) —entrygraph paths --source-category http_input --sink-category sql. Combine with--sourceto union both. - Precision/recall dial: by default only high-confidence edges are traversed.
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). - Sanitizers: a registered sanitizer for the sink's category called on a
path (e.g.
shlex.quote) discounts its risk score — heuristically, since there is no dataflow, so it never zeroes the risk or hides the path.--prune-sanitizedopts into dropping those paths entirely. - Target an exact sink with
--sink py:subprocess.runinstead of a category.
stats & --json
entrygraph stats
entrygraph --help # every command and flag
╭─────────────── index stats ────────────────╮
│ ┏━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓ │
│ ┃metric ┃ value┃ │
│ ┡━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩ │
│ │repo_root │ /path/to/acme-api │ │
│ │files │ 5│ │
│ │symbols │ 32│ │
│ │edges │ 34│ │
│ │entrypoints │ 5│ │
│ │sink_edges │ 2│ │
│ └─────────────────┴──────────────────────┘ │
╰────────────────────────────────────────────╯
Add --json to any query command for machine-readable output (paths include
risk_score and may_continue):
[
{
"risk_score": 0.4887,
"may_continue": false,
"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]
}
]
Colored tables, share/confidence bars, and risk-tree highlighting render in a real terminal; piped or
--jsonoutput is plain text.
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 (paths are risk-ranked, highest first)
paths = graph.paths(source="app.routes.*", sink_category="command_exec")
for p in paths:
print(p.risk_score, p.render(), "(+may continue)" if p.may_continue else "")
# 0.49 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
# 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",
prune_sanitized=True) # drop paths where a shlex.quote etc. is called
# 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/sanitizers — drop an
entrygraph.tomlin the repo root with[[sink]]/[[source]]/[[sanitizer]]tables (same schema as the built-indata/sinks/*.toml), or callentrygraph.detect.taint.register_sink(...)/register_sanitizer(...). A[[sanitizer]]called on a path discounts its risk score for that category; since reachability has no dataflow, the discount is capped (a match never zeroes risk or hides a path — use--prune-sanitizedto drop them explicitly). 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
Project details
Release history Release notifications | RSS feed
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.40.tar.gz.
File metadata
- Download URL: entrygraph-0.1.40.tar.gz
- Upload date:
- Size: 210.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9b6b9b4693cfd3476a523bda71f5751ef1ef0a42b466c7407fabb793f368aec0
|
|
| MD5 |
f40250f291c1ff24ef771f62f24b2f02
|
|
| BLAKE2b-256 |
95c4d9dc988007efcc751f7f1d960cae6af3269f319f69a75582901dc4714ae4
|
Provenance
The following attestation bundles were made for entrygraph-0.1.40.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.40.tar.gz -
Subject digest:
9b6b9b4693cfd3476a523bda71f5751ef1ef0a42b466c7407fabb793f368aec0 - Sigstore transparency entry: 2054195525
- Sigstore integration time:
-
Permalink:
brettbergin/entrygraph@20aaf8a81beed35ad59b3623115934d83712c816 -
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@20aaf8a81beed35ad59b3623115934d83712c816 -
Trigger Event:
push
-
Statement type:
File details
Details for the file entrygraph-0.1.40-py3-none-any.whl.
File metadata
- Download URL: entrygraph-0.1.40-py3-none-any.whl
- Upload date:
- Size: 164.1 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 |
331471b0528915d720d91e6112022e58346c1d373bd5dda2b966a8f43cbc6982
|
|
| MD5 |
814be7de12505f6b25a9fbd0289173d1
|
|
| BLAKE2b-256 |
aa9dd8c4fdbcc89e1cc780fb8cada8af861b2c0655c4f4be8ee3d1582e12a8fb
|
Provenance
The following attestation bundles were made for entrygraph-0.1.40-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.40-py3-none-any.whl -
Subject digest:
331471b0528915d720d91e6112022e58346c1d373bd5dda2b966a8f43cbc6982 - Sigstore transparency entry: 2054195722
- Sigstore integration time:
-
Permalink:
brettbergin/entrygraph@20aaf8a81beed35ad59b3623115934d83712c816 -
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@20aaf8a81beed35ad59b3623115934d83712c816 -
Trigger Event:
push
-
Statement type: