Inspiration
On 8 September 2025, someone phished an npm maintainer and published new versions of chalk, debug and 16 other packages with a crypto-stealer inside. Together they get over 2 billion downloads a week. The bad versions were up for about two hours.
What stuck with us was the first hour. Seventeen minutes after the release, no scanner had heard of it. The malware records reached OSV almost an hour later, and the write-ups came the next day. If your team merged a routine dependency bump that morning, every check passed, because they all ask the same question: is this version in an advisory database? Nothing was in the database yet.
Most of us don't even know what we depend on. You pick 30 packages; your lockfile has 600. The one that gets hijacked is usually four levels down, pulled in by something you never chose. We wanted a tool that watches all of it, judges a release by how it behaves, and helps you fix it instead of just warning you.
What it does
DepthMap runs in GitLab CI with a two-line include. On every push and merge request it reads the lockfile, builds the full dependency tree, and colours every package version green, amber or red.
A version goes red for things you can see before any advisory exists: it was published an hour ago, the publisher changed, provenance disappeared, an install script showed up. It also goes red for known malware and critical advisories. We compare red packages with real attacks (chalk/debug 2025, Shai-Hulud, ua-parser-js, event-stream), so the MR can say "this looks like the chalk/debug takeover, 4 of 4 signals".
If something is red, the gate fails the pipeline, and DepthMap leaves one comment on the merge request: what's wrong, where your code uses it (down to src/log.js:1), and the fix. That failure starts our GitLab Duo flow, which explains each package in plain language. If you comment "fix all", the flow edits package.json, commits to a branch and opens a fix merge request. A CI job regenerates the lockfile, DepthMap scans again, and a person reviews and merges.
There's also a web app. Every push gets a page with the tree and the paths to each risky package. An "All dependencies" map holds every package we've scanned, about 10,900 so far, with each version's advisories, the fixed version and the fix commit. Security watchers poll the places where attacks get reported first. An hourly job re-checks every repo, so one new advisory turns the same package red everywhere at once.
We replayed 8 September hour by hour, using only what was public at each moment. At 13:30 UTC, with no advisory anywhere, DepthMap already blocked the merge.
How we built it
The scanner is TypeScript on Node with Postgres. It reads npm and pnpm lockfiles, registry metadata, OSV and GitHub malware advisories. The key decision was caching by package version: a published version never changes, so we fetch its facts once and every repo reuses them. A repeat scan of a 640-package app takes half a second and makes zero network calls.
The upgrade advisor finds your imports with oxc, and compares the type definitions and module format of the old and new versions with the TypeScript compiler. That's how it knows chalk 5 breaks require('chalk'). When a fix crosses a breaking version, Claude Haiku reads the package's own migration notes and proposes the exact line edits. We check every edit against the file before showing it.
The Duo flow has four agents: one reads the scan result from the job log, one decides between explaining and fixing, one writes the explanation, and one makes the fix MR. CI does everything deterministic, so the agents only run when something is actually wrong.
The maps use a small Rust core compiled to WebAssembly for layout and path finding (50,000 nodes in about 7 ms), drawn with sigma.js. Every scan also records its carbon cost with the SCI method.
Challenges we ran into
Replaying the past honestly was harder than it sounds. The registry shows today's state, some feeds only give a date with no time, and versions get unpublished later. Every signal had to be filtered by when it became public, or the replay would cheat.
Avoiding false alarms took a lot of tuning. Big packages rotate maintainers all the time, so "new publisher" only counts when it's someone from outside the previous maintainer list. Build tools with a DoS bug are amber, but malware in a dev dependency stays red, because that's exactly how Shai-Hulud stole CI secrets.
An agent can't run npm install, so the flow can't regenerate a lockfile. We moved that into a CI job that runs only on fix branches, never runs install scripts, and pushes the result back.
Real repos broke our assumptions. pnpm 11 lockfiles contain two YAML documents, and we were reading the wrong one: Vue's monorepo came back with 15 packages instead of 635. On gitlab.com we also found a race, where an older pipeline could finish last and overwrite a "ready to merge" comment with "blocked". Both are fixed, and the fix for the race is covered by a test.
Accomplishments that we're proud of
The September replay: blocking a real attack seventeen minutes in, before any advisory existed, using only data that was public at the time.
The full loop running in public on gitlab.com. A failed gate starts the Duo flow, "fix all" opens a fix MR, the lockfile is regenerated, and the pipeline goes green. Nobody touches a terminal.
Telling people where their own code breaks, not just that a package is bad.
Making it cheap enough to run on every push. A repeat scan costs about 900 times less energy than the first one, because we never fetch the same thing twice.
What we learned
CVE scanners are blind to this kind of attack by design. OWASP now ranks supply-chain failures third and notes that only 11 CVEs map to the category. The signal that matters early is behaviour, not advisories.
Agents work best when they get a small, clear job and good input. Letting CI do the crawling and giving the flow a short summary made it faster, cheaper and more predictable than letting it explore on its own.
A person should still approve the fix. Upgrades break code, and a reviewed merge request is the right place for that decision.
What's next for DepthMap
Fix suggestions as GitLab "Apply suggestion" comments on the exact lines. More ecosystems after npm: PyPI and Cargo have the same problem. An organization-wide view, so a security team can see in one place which repos a new attack touches and which fixes are already open. Add support for Rust - Golang - python and other languajes
Built With
- anthropic-api
- claude
- docker
- gitlab
- gitlab-ci/cd
- gitlab-duo-agent-platform
- gitlab-pages
- graphology
- hono
- node.js
- oxc-parser
- playwright
- postgresql
- rust
- sigma.js
- typescript
- typescript-compiler-api
- vite
- vitest
- wasm-bindgen
- webassembly
Log in or sign up for Devpost to join the conversation.