Symbol-level supply chain reachability analysis, built on HydraDB.
Hack Hydra — Track 02, Repos, dependencies and code as graphs.
When a package on npm is compromised, existing tooling tells a team "you depend on this package." That is nearly useless. A mid-size application has 800–1500 transitive dependencies, so most alerts are noise — and teams respond by either panic-patching everything or ignoring alerts wholesale.
Sightline answers the question a security team actually has:
Does any code path from our entry points actually reach the vulnerable code?
It ingests a public npm dependency subgraph and a target repository's internal code graph, joins them at the import boundary, and reports which advisories are genuinely reachable — and, more usefully, which are provably not.
Depending on a package is not the same as executing its vulnerable code.
Here is Sightline's real output for the demo repository. Two advisories, both rated HIGH, opposite verdicts:
REACHED [email protected] CVSS 8.3 GHSA-qq9h-g4jm-xgf3
┌ src/index.ts#<module>
├→ src/server.ts#<module> :2
├→ src/modules/auth/auth.routes.ts#<module> :2
└→ [email protected]#toNodeHandler :7
NOT REACHED [email protected] CVSS 7.5 GHSA-w5hq-g745-h8pq
Present in the lockfile via @google-cloud/storage → gaxios → uuid.
Nothing in the codebase imports it. 0 paths.
The second one is the point. uuid carries a HIGH-severity advisory and sits in the
dependency tree — every scanner on the market will raise it. Sightline shows there is no
path to it from anywhere the application actually starts, so it can wait.
One detail worth noting: the string uuid does appear in the source, inside comments
like // Keys look like photo/<uuid>.jpg. A grep-based or text-similarity scanner flags
those files. Reachability over a real syntax tree does not.
The open-source HydraDB engine has no text index and no vector index. You cannot hand it a name and get relevant nodes back — a query must already know the integer id it is starting from. Sightline is built around that constraint rather than against it:
names, versions, symbols, user input
│
▼
┌────────────────────────┐
│ resolver layer │ ours: SQLite key→id map, prefix search,
│ (SQLite) │ edit distance, lockfile resolution
└───────────┬────────────┘
│ integer node ids
▼
┌────────────────────────┐
│ HydraDB │ bounded traversal, algo.MSpaths,
│ (open-source engine) │ whole-path results
└────────────────────────┘
Display strings stay in the sidecar and out of the traversal path. HydraDB walks integer ids; the sidecar resolves identity. That split is what keeps traversal fast, and it is the honest answer to "how did you use HydraDB well?"
ECOSYSTEM Package → Version → DEPENDS_ON* → Version ← AFFECTS ← Advisory
▲
the import boundary ────┤
▼
CODE Repo → File → Symbol → REACHES* → Symbol → REACHES → ExtSymbol
ExtSymbol is a symbol inside a dependency. Linking an internal Symbol to one is
what turns "this app depends on better-auth" into "line 7 of auth.routes.ts calls into
better-auth".
This section is the point of the project, so it is specific.
Reachability is a many-to-many bounded path search: from every entry point in the
codebase to every symbol of every vulnerable dependency version. That is exactly what
algo.MSpaths exists for — it resolves many indexed source and target values and
evaluates them together, in one call, instead of fanning out N×M queries from the client.
CALL algo.MSpaths({
sourceLabel: 'Symbol', sourceProperty: 'skey', sourceValues: [...entry symbols],
targetLabel: 'ExtSymbol', targetProperty: 'skey', targetValues: [...vulnerable symbols],
relTypes: ['REACHES'], relDirection: 'outgoing',
maxLen: 10, pathCount: 50, resultLimit: 500
}) YIELD path RETURN pathThe full reachability report for the demo repo — 6 advisories, 3,420 versions in the graph — resolves in 134 ms.
Given a compromised version, everything transitively depending on it is a reverse
transitive closure of unknown depth. algo.SSpaths with relDirection: 'incoming' walks
reverse adjacency from a fixed source and returns whole paths, so the product can show
through which chains something is exposed, not merely that it is.
On the ingested graph, [email protected] reaches 290 distinct dependents at depth 6 in
271 ms.
- Reverse transitive closure of unknown depth. In SQL this is one join per hop with no known bound; the query has to be generated per depth and degrades sharply. Similarity retrieval does not address it at all — "which packages are near this one" is a different question from "which packages reach this one".
- Whole paths instead of endpoints. A plain pattern match projects endpoints. The native path procedures return the chain, which is the entire product: the evidence, not the verdict.
- Many-to-many in one call. Without
algo.MSpaths, reachability becomes entrypoints × vulnerable-symbols round trips orchestrated client-side.
HydraDB implements a deliberate subset of OpenCypher and rejects the rest at parse time.
Several constraints shaped this codebase directly — no IN, no CONTAINS, no
min/max, a pass-through-only WITH, integer-only node ids, and bounded traversal
only. Every one is documented with the workaround, the measured cost, and the server's
verbatim rejection message, in docs/HYDRADB_NOTES.md.
That file also records two reproducible engine issues found while building, one of which (an intermittent first-query failure on fresh Bolt connections) is reported upstream.
Requires: Docker, Node.js ≥ 22.18, and pnpm.
Node 22.18+ matters — Sightline runs TypeScript directly through Node's native type stripping, so there is no build step.
pnpm hydra:upPulls ghcr.io/hydra-db/hydradb:latest and starts a local single node (Bolt 7687,
HTTP 8443, admin 9090). Sightline consumes HydraDB as a running service over the
network only — see Licensing.
Building from source instead works fine; follow the
upstream README. RUST_MIN_STACK=33554432 is
mandatory either way — without it the node serves /readyz and then aborts on the first
query.
pnpm installpnpm demoThis ingests two real repositories — directus/directus (2,701 packages, for blast
radius) and the committed demo application in fixtures/demo-app
(for reachability) — attaches real advisories from deps.dev, and builds the code graph.
It also runs npm ci inside the demo app. Import resolution needs the target's
dependencies on disk: the compiler follows better-auth/node into node_modules to
learn it belongs to the better-auth package. Without them, call resolution drops from
96% to 30%. That is true of any static analyser.
About two minutes cold, seconds afterwards — every HTTP response is cached to .cache/.
To analyse your own project instead:
DEMO_REPO=/path/to/your/app DEMO_SLUG=my/app pnpm demopnpm api # http://127.0.0.1:3001
pnpm web # http://127.0.0.1:5173Open http://127.0.0.1:5173. The Reachability tab is the one that matters.
| Packages | 2,333 |
| Versions | 3,420 |
| Maintainers | 1,121 |
| Advisories | 54 |
| Files / Symbols / ExtSymbols | 21 / 167 / 87 |
| Dependency edges | 6,700+ |
| Call-edge resolution | 439 / 456 — 96.3% |
All real: npm registry, deps.dev, and OSV-sourced advisories. No synthetic fixtures.
| Operation | Latency |
|---|---|
| Search (sidecar prefix) | 0.45 ms |
| Typosquat scan, 2,333 names | 1.7 ms |
| Exposure window | 1.9 ms |
| Shared-maintainer attribution | 3.0 ms |
| Reachability, full report | 134 ms |
| Vulnerability scan, 319 versions | 151 ms |
| Blast radius, depth 6 | 271 ms |
Full cookbook with the exact queries: docs/QUERIES.md.
Stated plainly, because the "not reached" column asserts safety.
- Call-edge resolution is 96.3%, not 100%. 17 calls on the demo repo did not resolve. An unresolved call could in principle hide a path. The number is reported in the UI.
- Dynamic dispatch and computed member access are not traced. Out of scope by design.
- Advisories name packages, not functions. The industry has no symbol-level vulnerability data, so every exported symbol of an affected version is treated as vulnerable. This makes reachability conservative — it over-reports reach rather than under-reporting it, which is the safe direction for a security tool.
- Maintainer data is current, not historical. The npm registry returns who maintains a
package now, not who published a given version, so
PUBLISHED_BYmeans "currently maintains". - Deep dependency resolution comes from deps.dev, computed at their crawl time, so it reflects what a fresh install would resolve rather than what a historical install did. Lockfile seeds are exact.
- TypeScript and JavaScript only. The code graph uses the TypeScript compiler.
| Document | Contents |
|---|---|
| docs/SCHEMA.md | Node labels, relationship types, and the identity scheme |
| docs/QUERIES.md | Verified Cypher cookbook with measured latencies |
| docs/HYDRADB_NOTES.md | Every subset constraint hit, and how it was handled |
| docs/DAY0_PLAN.md … DAY4 | The build log, including what was wrong and got changed |
Third-party data sources and libraries are credited in ATTRIBUTION.md.
Sightline is licensed under Apache-2.0 (LICENSE).
HydraDB itself is AGPL-3.0. Sightline is a separate application that communicates with it over the network (Bolt and HTTP) and does not link, vendor, fork, or copy its source. No HydraDB code is present in this repository; it is consumed as a running service only.