License Drift: How Your Codebase Quietly Picks Up GPL

Visual concept representing license compliance drift in an open source dependency

License changes happen quietly. A maintainer gets acquired, gets frustrated with permissive licensing, or just changes their mind about how they want their project used. They push a new release with an updated LICENSE file. Your lockfile pins to a caret range. Somewhere in your next npm install, you upgrade past the change.

Nobody checked the license. The CI pipeline turned green. The PR got merged. The AGPL-licensed code is now in your closed-source SaaS product, and the only person who doesn't know yet is your legal team.

Why AGPL-3.0 is the one that catches commercial teams off guard

Not all open source licenses create the same exposure. MIT, Apache-2.0, and BSD-2-Clause are genuinely permissive — you can use them in commercial products without publishing your own source. Most engineers know this and assume their dependencies are in this bucket.

AGPL-3.0 is different in a way that catches commercial software teams off guard. The "network use" clause extends the GPL copyleft obligation to software used to provide a service over a network — you don't have to distribute the software to trigger the requirement. If a package under AGPL-3.0 is in your dependency tree and you're running that code as a web service, you're obligated to make your complete corresponding source available to users of that service. The definition of "complete corresponding source" has been interpreted broadly enough that it is not a risk worth carrying without explicit legal review.

For an open source project, this is fine. For a closed-source SaaS company, one package in your transitive graph — a package you didn't choose, a package a package you chose pulled in — can create that exposure.

GPL-2.0 has the same copyleft structure, just without the network-use extension. SSPL-1.0 (used by MongoDB for server-side software since 2018) goes further still: it requires publishing the source of everything needed to run the software as a service, including your infrastructure configuration and management layers. These aren't theoretical concerns. They're licenses actively in use by packages with millions of weekly downloads.

How drift enters a codebase

The failure mode isn't usually "we knowingly added an AGPL package." The three routes that actually happen:

The transitive path. You add [email protected], which is MIT-licensed. Eighteen months later, the maintainer releases [email protected] and relicenses to AGPL-3.0. Your lockfile pins ^2.0.5. On the next npm install that picks up the latest compatible version, you're running AGPL-licensed code. The lockfile diff shows the version bump. Nobody was looking at it through a license lens.

The SPDX expression problem. Some packages switch from a permissive license to a dual-license model: free for non-commercial use, AGPL for commercial use. The package.json license field may contain an SPDX expression like MIT OR AGPL-3.0-only. A naive scanner that matches on the string "MIT" will pass this package. Correct SPDX expression parsing understands that OR in SPDX means the licensee can choose — but the licensor has made commercial use subject to AGPL. String matching is not license scanning.

The lockfile-less CI build. You didn't change any manifests. A transitive package two levels down released a new version overnight. Your CI runs npm install in a Dockerfile without COPY package-lock.json before the install step — a pattern that appears in codebases more often than teams realize. The resolved tree differs from what was tested last week. No one noticed because CI turned green.

The gap between stated policy and enforced policy

Most teams that have thought about license compliance operate at the level of "we use MIT and Apache-2.0." That's a policy intention. Very few have a mechanism that enforces it continuously against the full transitive graph.

The gap between stated policy and enforced policy is where license drift lives. npm audit won't catch it — CVE scanning and license scanning are categorically different tools. A manual quarterly review won't catch it reliably — packages release new versions daily, and a license can change between review cycles without anyone noticing until something gets flagged in a vendor security questionnaire or a legal review for an acquisition.

What works is policy-as-code: a config file in the repository that declares exactly which SPDX identifiers are allowed, which are denied, and which require explicit sign-off. That config runs on every PR that touches a manifest or lockfile. When something in the full transitive graph violates the policy, the build fails with a precise annotation on the diff line that introduced the change — not in a weekly report, at the moment of introduction.

What a working license policy config looks like

The config is simple. Here's a minimal example that covers the common commercial exposure cases:

policy:
  license:
    allow:
      - MIT
      - Apache-2.0
      - BSD-2-Clause
      - BSD-3-Clause
      - ISC
      - 0BSD
    deny:
      - AGPL-3.0
      - AGPL-3.0-only
      - AGPL-3.0-or-later
      - GPL-2.0
      - GPL-2.0-only
      - GPL-3.0
      - GPL-3.0-only
      - SSPL-1.0
    on_deny: fail
    on_unknown: warn

Two details worth explaining. The on_unknown: warn setting matters because some packages have non-standard LICENSE files that don't map cleanly to an SPDX identifier — custom license text, modified permissive licenses, or simply unclear language. Flagging these as warnings rather than failures lets you triage without blocking every build, while still surfacing them for review. Silent pass on unknown licenses is worse than a warning queue.

The deny list covers all SPDX variants explicitly. AGPL-3.0, AGPL-3.0-only, and AGPL-3.0-or-later are three different valid SPDX identifiers that different package authors use for what is functionally the same license intent. A scanner that matches only AGPL-3.0 will pass a package declaring AGPL-3.0-only in its manifest. The deny list must enumerate all variants.

Remediation paths when a violation surfaces

When a license policy violation surfaces, the remediation depends on where the package sits in the tree:

  • Direct dep violation: Replace it with a permissively licensed alternative or remove the feature it enables. This is the cleanest path — you have full control over the direct dep list.
  • Transitive dep via a direct dep: Pin the direct dep to a version before the license change. Open an issue with the upstream package to track whether a permissively-licensed fork or alternative emerges. Record the pin and the reason in a comment so future engineers don't remove it without understanding why it's there.
  • Transitive dep you can't immediately avoid: Get a legal opinion on whether your specific use triggers the obligation. The AGPL network-use clause is not triggered identically by all types of invocation — this is a legal question, not an engineering judgment call. But you need to know the package is there to ask the question at all.

The goal isn't zero open source dependencies — that's not a coherent strategy for any modern software team. It's knowing what's in your graph and making deliberate choices about what you accept and why. Most license drift incidents are not malicious. They're the result of not watching the transitive graph with the same care as the direct dep list.

Depswright evaluates every package in your full dependency graph against your license policy on every scan. Try it free.