Compliance Intake · Dependency Ledger

License Drift: The Compliance Incident That Never Files a Ticket

A transitive dependency's license changes on a routine version bump. Nothing throws an error, nothing pages on-call, and the pipeline goes green exactly as it did yesterday. The first person to notice is usually legal or procurement — months later, after the package has already shipped to production a dozen times.

This isn't a legal lecture. It's a monitoring problem that most pipelines don't monitor. A license is metadata attached to a dependency the same way a CVE score is — it changes state, it has a severity, and it can turn a routine npm install into an obligation nobody agreed to. Treated as an observability gap instead of a contracts question, it's a problem platform and DevOps teams are already equipped to close.

Exhibit A — Dependency Manifest (Sample) 05 entries flagged for review
Package Declared License Observed Risk Status
ui-datepicker-lite MIT → BSL 1.1relicensed at v4.2.0, three minor versions back Production usage predates the change Flagged
pdf-render-core MITunchanged across last 9 releases None observed Cleared
queue-worker-utils Apache-2.0pulls in a GPL-2.0 fork two levels down Copyleft term inherited transitively Review
chart-fx Custom / no-compete clausefree to use, restricted on redistribution as a competing product Permissive-looking, not permissive Review
log-shipper AGPL-3.0commercial tier available, community tier is AGPL by default Network-use copyleft trigger Flagged
Docket Entries 04 findings
LR-001 Silent Risk / Supply Chain Unmonitored

The incident that doesn't page anyone

A transitive dependency's license changes — or a permissively licensed package gets relicensed to something restrictive — and no build fails, no alert fires, no dashboard turns red. This is exactly what happened at industry scale when Elastic moved Elasticsearch and Kibana from Apache 2.0 to the Server Side Public License, and again with MongoDB's SSPL shift years earlier: the software didn't change, only the terms governing its use did, and every downstream consumer inherited that change the moment they upgraded.

FindingLicense state is mutable and unmonitored in most pipelines — the same blind spot vulnerability scanning had before SCA tooling made it routine.

LR-002 Ownership / Process Reframed

Why this is a DevOps problem, not just a legal one

Legal can write the policy — which licenses are acceptable, which trigger review, which are outright disallowed — but legal isn't in the dependency graph at 2am when a lockfile updates. That's the same argument that moved vulnerability scanning out of periodic audits and into CI. A Software Bill of Materials, in SPDX or CycloneDX format, is the artifact that makes license state queryable the way a vulnerability database makes CVE state queryable.

FindingLicense compliance scales the same way security scanning does — by moving from a periodic review to a continuous, automated check with a clear owner.

LR-003 Scan Surface / Classification Under Review

What to actually scan for

Permissive vs. copyleft is the binary most teams check for, and it's not enough. Four patterns cause most real incidents:

FindingA binary permissive/copyleft check misses the two categories — dual-licensing and use-case restrictions — that cause the most surprising incidents.

LR-004 Enforcement / CI Gate Gate Installed

Building it into CI

Once the policy exists — an allowlist, a review list, a denylist — enforcement is mechanical. Tools like license-checker for npm, FOSSA, and the OSS Review Toolkit (ORT) can walk a full dependency tree, including transitive dependencies, and classify every license found against policy.

FindingLicense scanning belongs in the same gate as dependency vulnerability scanning — a failed check before merge, not a finding after release.

Exhibit B — CI Enforcement license-gate.ts
ci / license-gate.ts exit 1 on violation
// Runs after `npm ls --all --json` produces the resolved dependency tree.
// Fails the pipeline on any denylisted or unreviewed license.

import { readFileSync } from "fs";

type Policy = {
  allow: string[];
  review: string[];
  deny: string[];
};

type DependencyRecord = {
  name: string;
  version: string;
  license: string;
};

function classify(dep: DependencyRecord, policy: Policy): string {
  if (policy.deny.includes(dep.license)) return "DENY";
  if (policy.review.includes(dep.license)) return "REVIEW";
  if (policy.allow.includes(dep.license)) return "ALLOW";
  return "UNKNOWN"; // unrecognized license — treat as a gate failure, not a pass
}

function runGate() {
  const policy: Policy = JSON.parse(readFileSync("license-policy.json", "utf-8"));
  const deps: DependencyRecord[] = JSON.parse(readFileSync("resolved-deps.json", "utf-8"));

  const denied = deps.filter(d => classify(d, policy) === "DENY");
  const unknown = deps.filter(d => classify(d, policy) === "UNKNOWN");
  const review = deps.filter(d => classify(d, policy) === "REVIEW");

  if (review.length) {
    console.log(`${review.length} dependencies require manual review — routing to #license-review.`);
  }

  if (denied.length || unknown.length) {
    console.error("License gate failed:");
    [...denied, ...unknown].forEach(d =>
      console.error(`  ${d.name}@${d.version} — ${d.license}`)
    );
    process.exit(1);
  }

  console.log("License gate passed.");
}

runGate();
Audit Note

None of this replaces legal review — it replaces the assumption that legal review happens before every dependency update ships, which in practice it never does. The gate's job is narrow: catch drift the moment it enters the tree, route the ambiguous cases to a human, and let everything already cleared move at normal CI speed. The instinct to build is the same one that made vulnerability scanning routine — treat a changing piece of metadata as something worth watching continuously, not something worth discovering after the fact.

Next in this series Schema migrations at scale — running them without locking the table your traffic depends on.