Ask a supply-chain tool “what does this repository depend on?” and you usually get one answer: a single flattened list for the whole repo. For a monorepo, that answer is wrong in a way that matters.
A monorepo isn’t one project. It’s a checkout service, a data pipeline, an internal SDK,
a web app — each with its own dependency footprint, its own blast radius, its own owner.
services/checkout pulling a compromised lodash is a different incident from
tools/scripts doing the same. Collapse them into one list and you lose exactly the
resolution an operator needs: which part of my codebase actually touched the bad thing?
The usual answer: parse the manifests
The obvious way to recover per-package structure is to read it out of the repo — walk the
package.json / pyproject.toml / pom.xml files, reconstruct the workspace graph, and
map dependencies back to packages. It works, sort of, until it doesn’t: workspace tools
disagree on layout, generated and vendored code lies, private registries hide edges, and
the declared manifest is not the resolved, installed set. You end up maintaining a
parser per ecosystem to approximate something you still can’t fully trust.
We don’t do that.
Discovered from traffic, not declared
Every install in your CI already crosses Upwarden — that’s the whole point of a registry firewall. So instead of asking the repo to describe itself, we watch what it does.
When a CI job installs through the proxy, its request carries a short-lived, keyless
OIDC identity minted by the CI provider. Upwarden verifies it and reads the attested
claims — repository, ref, commit, workflow, run_id. That ties every fetch to a
specific run, and the fetch paths reveal which independent part of the repo asked for
what. The seams between your units show up on their own, because different units pull
different things. No package.json parsing. No manifest to keep in sync. No per-repo
setup beyond the two-line drop-in. The structure is discovered from observed traffic,
not declared.
There’s an honesty cost to being traffic-driven, and we’d rather state it than paper over it: this is what a run pulled, not a lockfile. A unit appears once a verified run has installed through the firewall; a cold cache or an un-wired tool means less crosses the wire. So the per-unit view is a trustworthy lower bound — “most observed,” never “complete.” It’s an audit record of real traffic, not a bill of materials. For catching what actually reached a build, a faithful record of reality beats an aspirational declaration.
What falls out once you can see the units
Once the units are visible, per-package supply-chain visibility stops being a project and starts being a side effect. Open a unit and you get its run-scoped audit lens — everything that one CI run pulled through the firewall, blocked-first, with every block traced to the CVE or advisory that stopped it:
services/checkoutpulledlodash@4.17.11→ blocked, CVE-2021-23337packages/apipulledws@7.4.5→ blocked, CVE-2024-37890apps/webpulledreact@18.3.1→ served
Not “somewhere in this repo, something was blocked.” This unit, this run, this version, this CVE. A verdict you can follow to a citation, scoped to the part of the codebase that actually owns it.
The monorepo was always many supply chains. The only question was whether your tooling could see the seams — and whether it had to be told where they were, or could just watch, and know.
See the per-unit lens in the dashboard docs, or the model behind it in units and run scope.