Built on HydraDB · OpenCypher over object storage

A compromised package is a graph problem.

ChainTrace turns your npm and Python lockfiles into one dependency graph, then walks it backwards from any compromised version to the services that actually ship it — with hop counts, the attack path, and a risk score that weighs production first.

npm · bun · pip · poetry · pipenv — lockfile parsed, ranges resolved

Blast radius — livedepth 5

from npm:axios@1.7.2

checkout-api2 hopsCRITICAL
billing-worker3 hopsCRITICAL
docs-site3 hopsMEDIUM
risk 0/100 · 2 production services reached
0
Hops per traversal — depth is capped, not unbounded
0
Lockfile parsers: npm, bun, pip, poetry, pipenv
0
Risk scale per service — production weighted heaviest
0
Ecosystems in one graph — npm and PyPI

The problem

Supply chain tools count packages. Attacks travel edges.

Failure modes
03
Shared cause
the graph is thrown away

01Count

A list is not an impact

A scanner hands you a count of vulnerable packages. Nothing in that list tells you whether the bad version ships in checkout or sits in a test fixture nobody runs.

02Depth

The reach is transitive

The package that breaks you is three hops down someone else's tree, pulled in by a semver range you never wrote and cannot see from your own manifest.

03Ownership

Services carry the risk

Risk lands on the service that ships the code, and on the team that owns it. Package-level reports cannot tell you who gets paged tonight.

The approach

Store the edges. Then ask backwards.

Every lockfile becomes vertices and edges in HydraDB, with your services attached to the versions they ship. Ranges are resolved to concrete versions on the way in, so the graph holds what you actually install.

(:Package)-[:HAS_VERSION]->(:Version)
(:Version)-[:DEPENDS_ON]->(:Version)
(:Service)-[:DEPENDS_ON_VERSION]->(:Version)
(:Maintainer)-[:MAINTAINS]->(:Package)
  • Blast radius: which services a bad version reaches, and in how many hops
  • Attack path: the exact chain from service to compromised version
  • Risk: scored per service, production weighted heaviest
  • Co-maintainers: what else the same publishing account can reach
  • One pinned snapshot per query — the answer never straddles a write
chaintrace — cli

Point it at a repo. It finds the lockfile.

The CLI parses package-lock.json, bun.lock, requirements.txt, poetry.lock and Pipfile.lock, resolves every range to the version the installer would actually pick, and ingests the result. pnpm and yarn lockfiles are detected but not parsed yet. Package coordinates leave your machine; source files never do.

npm + PyPIRange resolutionLocal parse
chaintrace — scan

The tree, stored as a graph. Not a flattened list.

Packages, versions and DEPENDS_ON edges live in HydraDB with the dependency type and the range that pulled them in, npm and PyPI in the same graph. Traversal runs level by level, so you can ask for depth 2 and get depth 2 — not a query that walks your whole graph.

OpenCyphernpm: and pypi: keysDepth 1–5
GET /packages/axios/graph?depth=2
package
axios
depth
2
nodes
1,896 — Version vertices with hop depth
edges
3,910 — DEPENDS_ON, typed and ranged
model
Package → HAS_VERSION → Version
read
one pinned snapshot

One bad version. Every service it reaches.

Give it a version key and it walks DEPENDS_ON in reverse, collecting the services that ship it and the number of hops each one sits at. That hop count is the difference between a package you upgrade this quarter and one you page someone for tonight.

Reverse traversalHop distancePer-service
Blast radius — npm:axios@1.7.2depth 3 / 5
Blast radius of axios@1.7.2Reverse DEPENDS_ON traversal from a compromised version to the services that ship it, with severity scored per service.HOP 0HOP 1HOP 2SERVICESaxios@1.7.2compromisedacme-sdk@4.2.0http-client@2.1.4telemetry@0.9.1auth-kit@3.0.1pay-core@7.4.0web-utils@1.2.9log-ship@5.5.2checkout-apiproductionCRITICALbilling-workerproductionCRITICALdocs-sitestagingMEDIUM
14 DEPENDS_ON edges walked3 services reached2 in production

The chain, not the verdict. Every link named.

A score you cannot audit is a score nobody acts on. Attack paths return the actual chain — service, then each version in between, then the compromised one — with the range that pulled each link in, so you can see exactly where to cut.

Full chainRanges shownAuditable
Attack path — 3 hops1 of 4
checkout-apiservice · production
pay-core@7.4.0runtime · ^7.4.0
http-client@2.1.4runtime · ^2.1.0
axios@1.7.2compromised

Scored where it lands. On the service.

Production counts for 60 points, staging for 30. A direct dependency adds 30, one hop adds 20, three or fewer adds 10. Every score arrives with the reasons that produced it, so the ranking is arguable — which is the point.

0–100Reasons attachedEnvironment aware
Risk — billing-workerCRITICAL
0/100
Affected production service+60
One-hop transitive dependency+20
Reached within 3 hops+10
Team owns 4 downstream services+2

Who it's for

Built for the people who get paged.

Roles
03
Shared question
what does this actually reach?

SECURITY

Triage by reach

Sort an advisory feed by which services it actually touches, and close the ones that reach nothing without a meeting about it.

PLATFORM

Know who to page

Every affected service carries its repo, team and environment, so the list of people who need to act writes itself.

ENGINEERING

Upgrade what matters

Hop distance and dependency type tell you which upgrade removes real exposure and which one just moves a number.

How it works

Three steps, in order.

Setup
one command
Then
every version is queryable
01 / 03

Scan the repo

chaintrace scan reads the lockfile, resolves ranges, and writes packages, versions and DEPENDS_ON edges into the graph.

chaintrace scan
02 / 03

Register the services

A service names its repo, team and environment, and the versions it ships. This is what turns a package graph into an impact graph.

POST /services
03 / 03

Ask backwards

Hand any version key to blast radius, attack path or risk, and get the services, the chains, and the scores with their reasons.

GET /blast-radius

Questions

The short answers.

How is this different from the scanner I already run?

A scanner tells you a vulnerable version is present. ChainTrace tells you which of your services ship it, how many hops away it sits, and what the chain looks like. It answers impact, not presence — and it is meant to sit next to your scanner, taking its findings as input.

Which lockfiles does it read?

For npm: package-lock.json, npm-shrinkwrap.json and bun.lock (including the binary bun.lockb). For Python: requirements.txt, poetry.lock and Pipfile.lock. pnpm-lock.yaml and yarn.lock are detected but not parsed yet. The CLI picks the format out of the project directory, so there is nothing to configure.

How is the risk score calculated?

Per affected service, out of 100. A production service adds 60, staging 30, anything else 10. A direct dependency adds 30, one hop 20, and three hops or fewer 10. Every score returns the list of reasons that produced it.

Why a graph database?

Because the questions are all traversals. Reaching backwards from a version to the services that depend on it, at bounded depth, is a graph walk — not a join. HydraDB keeps the graph durable in object storage and reads every query from one pinned snapshot, so a traversal never straddles a write.

How deep does traversal go?

Up to five hops per query, and the depth is a parameter. The cap is deliberate: an unbounded walk across a real npm graph is not a query anyone should be able to trigger by accident.

Does my source code leave my machine?

No. The CLI parses the lockfile locally and sends package coordinates — name, version, range — to the ingest API. It does not read or upload source files.

Does it cover Python as well as npm?

Yes. PyPI packages are ingested from pypi.org, PEP 508 dependency strings are normalised into the same shape as npm's, and everything lands in one graph — keys are prefixed npm: or pypi:. Blast radius, attack paths and risk work the same either way, so a service that ships both is scored across both.

Get started

Stop counting vulnerable packages.

Start tracing what they reach.

npx chaintrace scan