RunTerror/sightline

★ 0Forks 0TypeScriptGitHub ↗Compare

README

Sightline

Symbol-level supply chain reachability analysis, built on HydraDB.

Hack Hydra — Track 02, Repos, dependencies and code as graphs.


What this does

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.

The insight

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.

Architecture

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?"

The two graphs, and where they join

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".

How HydraDB is used

This section is the point of the project, so it is specific.

algo.MSpaths is load-bearing, not decorative

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 path

The full reachability report for the demo repo — 6 advisories, 3,420 versions in the graph — resolves in 134 ms.

Bounded reverse traversal is the blast radius

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.

What this project would lose without HydraDB

  • 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.

Working within the Cypher subset

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.

Setup

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.

1. Start HydraDB

pnpm hydra:up

Pulls 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.

2. Install

pnpm install

3. Load the demo data

pnpm demo

This 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 demo

4. Run it

pnpm api    # http://127.0.0.1:3001
pnpm web    # http://127.0.0.1:5173

Open http://127.0.0.1:5173. The Reachability tab is the one that matters.

What is in the graph

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.

Measured latency

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.

Limitations

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_BY means "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.

Documentation

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

Attribution

Third-party data sources and libraries are credited in ATTRIBUTION.md.

License

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.

Contributors

abhishekbbbbbb

Issues