OSPulse

Dependency drift

You reviewed that dependency. It was fine. The review was accurate on the day you did it — and it has been quietly expiring ever since, because the thing you approved keeps changing and nothing tells you when it does.

The decision has a shelf life

Almost every organisation has a moment where an open-source dependency gets evaluated. Someone checks the licence, glances at the repository, notices the commit history looks healthy, and the library goes into the build. That decision is then treated as permanent. It goes into an approved-components list, or a spreadsheet, or nowhere at all, and the code ships.

The problem is that a dependency is not an artefact you acquired. It is a relationship with a project that keeps moving. The maintainer who made it trustworthy can leave. The release cadence that made it look alive can stop. The licence can change. The ownership can transfer to an account you have never heard of. Your own lockfile can pull in forty new transitive packages because a direct dependency you did not touch published a minor version.

None of that generates an event in your systems. There is no failing build, no red pipeline, no notification. The dependency continues to work exactly as it did, which is precisely why the decay is invisible. Drift is the gap between the dependency you approved and the dependency you now have.

The asymmetry that makes this hard

Adding a dependency is a deliberate act with a review attached. Everything that happens to it afterwards is passive, gradual, and unowned. Organisations spend nearly all of their governance effort on the moment of adoption, and almost none on the years that follow — which is where the risk actually accumulates.

Five kinds of drift

Drift is not one phenomenon. It is five, and they have different causes, different detection methods, and different remedies. Conflating them is why "we should update our dependencies" is such a useless instruction.

Version drift

The distance between the version you resolve and what upstream considers current.

Some lag is healthy — pinning to a version that has been in the wild for a few weeks is a reasonable defence against bad releases and against compromised ones. Lag becomes a risk when it crosses the line where the upgrade is no longer routine: when the maintainer has stopped patching your major version, when the jump is now three majors wide, when a security fix exists only on a branch you cannot reach without a rewrite.

The dangerous property of version drift is that it compounds silently and then presents as a crisis. Nobody schedules the work while it is a one-line bump. It becomes visible on the day an advisory lands and the fix is nine versions and two breaking changes away, in an incident, under time pressure.

Maintenance drift

The project itself slowing down or stopping.

This is the most common and the least detected form of drift. There is no announcement when a maintainer loses interest. Issues stay open a bit longer. Pull requests sit. Releases stretch from monthly to quarterly to never. Every individual month looks unremarkable; the trend across two years is unmistakable.

Maintenance drift matters because it determines what happens when you need something. An unmaintained dependency is not a problem until you find a bug, or need a patch, or need it to work on a new runtime — at which point you discover you have quietly become the maintainer.

Occasionally the decline is explicit rather than gradual. event-stream in 2018 is the canonical case: the original author, no longer interested, handed publishing rights to a volunteer who asked for them. The volunteer added a malicious transitive dependency. The abandonment came first; the compromise was what abandonment made possible.

Risk drift

The security posture of a version you already ship changing underneath you.

You did not change anything. The world did. A CVE is published against the exact version in your lockfile. A maintainer account is compromised and a malicious release goes out. A dependency turns out to have been backdoored for months before anyone noticed, as with xz-utils (CVE-2024-3094) in 2024, where a contributor spent nearly two years building trust before introducing the backdoor.

Risk drift is the only category most organisations detect at all, because it is what vulnerability scanners are built for. Even here, detection is partial: scanners see published advisories, and the interval between compromise and publication is exactly the interval in which you are exposed and unaware.

Licence drift

The terms you are operating under changing between versions.

Licences are treated as a fixed property of a package, checked once at adoption. They are not fixed. Projects relicence — from permissive to copyleft, from open source to source-available, from clearly stated to ambiguous — and a relicence can arrive in a patch release that your automation merges without a human ever reading the diff.

The consequences surface late and expensively: during an acquisition, a customer security review, or a compliance audit, when someone finally reconciles your declared licence position against what your lockfile actually resolves.

Transitive drift

Your dependency graph changing without you changing anything.

This is the one that surprises people. You have twelve direct dependencies and eight hundred packages in your lockfile. Almost none of that graph was chosen by anyone at your organisation. When a direct dependency publishes a release that adds, removes, or re-ranges its own dependencies, your graph reshapes — and if your ranges permit it, that reshaping can happen on a clean install without a single line of your code changing.

Transitive drift is how ua-parser-js (2021) reached so many builds, and how colors.js and faker.js (2022) broke thousands of applications when their author deliberately sabotaged them. Nobody chose those packages. They arrived as an implementation detail of something else, and they had the same execution privileges as code the team wrote.

Most of your attack surface was never reviewed

In a typical application the ratio of transitive to direct dependencies is roughly an order of magnitude. Whatever review process you apply to direct dependencies covers a small minority of the code you actually ship — and none of it applies to packages that join your graph later.

Why vulnerability scanners miss this

Vulnerability scanning is necessary and OSPulse does it. But a scanner answers one question:

Does this exact version appear in a published advisory database?

That is a question about paperwork that already exists. Four of the five drift categories above produce no advisory at all, so a clean scan is not evidence of health — it is evidence that nothing has been formally filed.

DriftDoes a CVE scanner see it?
Version driftNo. Being far behind upstream is not a vulnerability
Maintenance driftNo. Abandonment is never a CVE
Risk driftPartly, and only after publication
Licence driftNo. Wrong licence is not a security finding
Transitive driftOnly if the newly-arrived package already has an advisory

There is also a timing problem inside the one category scanners do cover. The lifecycle of an advisory runs: compromise, discovery, disclosure, publication, ingestion, scan. Your scanner sits at the end of that chain. The window that matters most — between a malicious release being published and the world finding out — is precisely the window in which the scanner is silent. Dependency confusion, demonstrated by Alex Birsan in 2021, never produces a CVE at all: nothing is vulnerable, an internal package name is simply resolved from a public registry instead of a private one. Nor did polyfill.io in 2024, where a script that had been trusted for years began serving malicious payloads after the domain changed hands.

Detecting these requires watching for change in the project, not looking up records about the code.

How OSPulse measures drift

The mechanism is deliberately simple: score continuously, store every snapshot, and treat the derivative as the finding.

Resolve the real graph

Drift is measured against what you actually resolve, not what your manifest declares. OSPulse parses lockfiles where they exist to get exact transitive resolution, so a graph change caused by a range expanding is visible as a change rather than hidden inside a range. See Ecosystems and parsing.

bash
curl https://api.ospulse.app/api/v1/repositories/$REPO_ID/dependencies \
  -H "Authorization: Bearer $OSPULSE_TOKEN"

Re-observe on a cadence, not on demand

Scans run on a schedule and on source-control events, not only when someone remembers. A dependency that has not changed still gets re-observed, because the project behind it may have changed even though the version has not. That is the entire point: drift happens to static dependencies.

Store snapshots, then difference them

Each observation produces a dated health snapshot with its per-category component scores and the evidence bundle behind it. The series is queryable, so the question "what changed, and when did it start" has an answer.

bash
curl "https://api.ospulse.app/api/v1/packages/$PACKAGE_ID/health/history?take=52" \
  -H "Authorization: Bearer $OSPULSE_TOKEN"

Attribute movement to a cause

A delta with no explanation is noise. POST /api/v1/ai/score-change-root-cause identifies which signal category moved and what observation drove it, so a drop becomes "the sole maintainer stopped merging in March" rather than "score fell by nine".

Alert on the change, not the state

Alert rules fire on movement — a score falling by a threshold, a new advisory affecting a resolved version, a licence changing, a package being deprecated, a new transitive package appearing in a graph. Alerting on state produces a permanent backlog nobody reads; alerting on change produces a queue with an end. See Alerts.

Watching the estate, not one package

Per-package history is for investigation. For steering, aggregate the same signal upward:

  • GET /api/v1/analytics/trends — how your portfolio's risk profile is moving over time. The direction of this line is the only dependency metric worth putting in front of an executive.
  • GET /api/v1/analytics/trends/{day}/contributors — for any day the trend moved, which packages and repositories caused it.
  • GET /api/v1/repositories/{repositoryId}/risk-summary — drift concentrated in one codebase, which is usually where remediation gets scheduled.
  • GET /api/v1/packages/watched — the packages you have explicitly put on watch, with their current health. Use this for the dependencies that would hurt most, regardless of what they score today.

Acting on drift

The failure mode of drift detection is a queue that grows faster than anyone can service it. Three things keep it tractable.

Separate "changed" from "matters". A score movement on a development-only dependency in an archived repository is not the same finding as the same movement in a service that handles customer data. GET /api/v1/packages/{packageId}/usage tells you where the package actually appears; repository criticality tells you whether anyone should care.

Schedule maintenance drift, escalate risk drift. Abandonment is a planning problem — it belongs in a roadmap, with a migration owner and a quarter. A malicious release is an incident. Routing both into the same queue guarantees one of them gets the wrong response.

Plan the exit before you need it. When a package has drifted past recovery, GET /api/v1/packages/{packageId}/replacement-candidates gives you actively-maintained alternatives with their own health context, and POST /api/v1/ai/remediation will draft the upgrade or migration path. The cheapest time to replace a dependency is the quarter before the incident.

Set the review to expire

If your organisation keeps an approved-components list, give entries an expiry rather than treating approval as permanent. A dependency approved three years ago has, statistically, already drifted. Continuous scoring is what makes re-approval cheap enough to actually do.

Next steps