Back to glossary

License Compliance

Open source license compliance is meeting the conditions licenses attach to using, modifying, and distributing the code you depend on.

Also known as open source license compliance, OSS license compliance, software license compliance

License compliance is meeting the conditions that software licenses attach to copying, modifying, and distributing code. For open source license compliance, that means finding every license present in your dependency tree, knowing which actions trigger obligations such as attribution or source disclosure, and satisfying them before you ship. Failing a condition can leave you using the code without a license.

The mechanism is copyright. Code is all rights reserved by default, and a license is a conditional permission: you may copy, change, and redistribute this, provided you do certain things. Most conditions only apply when you take a covered action, usually distributing a copy. The hard part of compliance is rarely reading licenses. It is knowing which licenses are actually inside what you ship. Registries show one declared license per package version, copied from a single metadata field. The Django 5.1.1 wheel on PyPI declares BSD-3-Clause and contains ten license files covering seven distinct licenses. Our position: declared license metadata is a claim made by the publisher, not a fact about the package. Compliance has to be computed from package contents, per version, across every transitive dependency, and checked before code merges.

What is software license compliance?

#

Software license compliance is the practice of keeping every use, modification, and redistribution of third-party code inside the permission its license grants. Copyright gives an author exclusive rights to copy, modify, and distribute a work as soon as it is written down. Everyone else needs permission, and a license is that permission with conditions attached.

Compliance therefore depends on three variables, and all three have to be known at once:

  1. What is in the artifact. Every license that applies to code, data, fonts, and images inside the thing you distribute.
  2. What you do with it. Running it internally, serving it over a network, shipping a binary, or publishing a library.
  3. What each license requires for that action. Keeping a notice, offering source, licensing your own changes the same way, or nothing at all.

The second variable decides most outcomes. The GPL's obligations attach to "conveying" a work, and the license defines that term narrowly. In the words of GPLv3 section 0: "Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying." The same GPL dependency is a non-event in a server you operate and a source-disclosure obligation in a binary you hand to customers.

For commercial software, "license compliance" also means seat counts and audit clauses in a contract. This entry covers open source dependencies, where the conditions live in license text and the count that matters is components, not seats.

What is an open source license?

#

An open source license is a copyright license that meets the Open Source Definition: anyone may use, study, modify, and redistribute the code, for any purpose. Criterion 6 is the one most often violated by licenses that call themselves open: "The license must not restrict anyone from making use of the program in a specific field of endeavor." A license that forbids offering the software as a managed service fails it, and so does one that conditions service use on releasing your entire service stack, which is what SSPL does. SSPL is not OSI-approved.

Open source licenses split by what they ask of you when you redistribute:

  • Permissive licenses such as MIT, BSD-3-Clause, and Apache-2.0 require you to keep copyright and license notices. Apache-2.0 adds a patent grant and NOTICE file handling.
  • Weak copyleft licenses such as LGPL and MPL-2.0 require you to share changes to the licensed files or library, but not the rest of your program.
  • Strong copyleft licenses such as the GPL require the whole combined work you distribute to be offered under the same license, with source.
  • Network copyleft licenses such as the AGPL extend that to users who reach a modified version over a network.

Package metadata increasingly states licenses with SPDX identifiers and expressions. MIT AND Apache-2.0 means both sets of terms apply. MIT OR Apache-2.0 means you choose one. GPL-2.0-only WITH Classpath-exception-2.0 means the GPL with an added permission that changes what linking triggers. As of September 2026, SPDX License List 3.29.0 has identifiers for more than 700 licenses. The OR operator matters in practice: a dependency tree that looks copyleft can have a compliant all-permissive path, and a compliance check should find it instead of failing.

"Source-available" licenses are not open source licenses. Redis 7.4 moved from BSD-3-Clause to a choice of RSALv2 or SSPLv1 in March 2024, and both restrict offering Redis as a managed service. A license list that only distinguishes "permissive" from "copyleft" will misfile these.

What triggers open source license obligations?

#

Open source license obligations are triggered by how you ship code, not by whether you use it. The table below maps the common readings for each product shape. It is engineering guidance, not legal advice. Linking, "derivative work", and whether a container image counts as distribution are contested, and the edge cases need counsel.

How the code leaves your handsPermissive (MIT, BSD, Apache-2.0)Weak copyleft (LGPL, MPL-2.0)Strong copyleft (GPL)Network copyleft (AGPL)
Internal tools and CI onlyNothingNothingNothingSource offer to users if you modify it
SaaS backend, never shippedNothingNothingNothing: no copy is conveyedSource offer to network users of a modified version
JavaScript sent to browsersNotices in what you shipSource of changed library filesCombined work under GPLCombined work under AGPL
Binary, mobile app, firmware, or image given to customersNotices, plus NOTICE file for Apache-2.0Source of changed library files; LGPL relinking termsSource of the combined work under GPLSame as GPL, plus network clause
Open source library you publishKeep notices; your license must be compatibleCompatible licenseYour users inherit the GPLYour users inherit the AGPL

Verdict: set license policy per product shape, not as one global deny list. A SaaS backend can carry GPL dependencies and stay compliant. The same dependencies inside a browser bundle or a container image you give customers are a problem. Deny AGPL by default only where you might modify it or cannot answer how it is deployed.

The browser row surprises people. A frontend bundle is a distributed copy, so every permissive dependency in it carries a notice obligation. Minifiers such as terser and esbuild keep only comments written as /*! or tagged @license. We ran terser 5.51.2 and esbuild 0.28.2 on a file with both styles plus a plain comment:

Bash
$ npx terser in.js
/*!
 * lib v1 | MIT License | (c) Jane
 */
/** @license MIT (c) Bob */
function a(){return 1}module.exports=a;

The plain comment is gone, and so is every notice that lived only in a dependency's LICENSE file, because that file is never bundled. That is how a team falls out of compliance with MIT, one of the most permissive licenses in wide use, without anyone noticing.

Why registry license metadata is not enough

#

Registry license metadata is one string chosen by the publisher, and registries do not check it against the package contents. Here is the Django 5.1.1 wheel, downloaded from PyPI:

Bash
$ pip download django==5.1.1 --no-deps -q
$ unzip -p Django-5.1.1-py3-none-any.whl 'Django-5.1.1.dist-info/METADATA' | grep -E '^License'
License: BSD-3-Clause
License-File: LICENSE
License-File: LICENSE.python
License-File: AUTHORS
$ unzip -l Django-5.1.1-py3-none-any.whl | grep -icE 'licen'
10

What PyPI declares

License: BSD-3-Clause
License-File: LICENSE
License-File: LICENSE.python
License-File: AUTHORS

1 license

One SPDX identifier in METADATA. This is the value registries, badges, and manifest-based SBOMs report.

What the wheel contains: 10 license files

  • dist-info/LICENSEdeclaredBSD-3-Clause
  • dist-info/LICENSE.pythondeclaredPSF, BeOpen, CNRI, CWI, 0BSD
  • contrib/gis/gdal/LICENSEBSD-3-Clause
  • contrib/gis/geos/LICENSEBSD-3-Clause
  • dispatch/license.txtBSD-style (PyDispatcher)
  • admin/static/…/img/LICENSEMIT (Code Charm)
  • admin/static/…/jquery/LICENSE.txtMIT (OpenJS)
  • admin/static/…/select2/LICENSE.mdMIT (Select2)
  • admin/static/…/LICENSE-SELECT2.mdMIT (Select2)
  • admin/static/…/xregexp/LICENSE.txtMIT (S. Levithan)

Seven distinct licenses. The sdist adds CC BY-SA 3.0 test data and Font Awesome under CC BY 4.0, OFL-1.1, and MIT.

File in the wheelLicenseListed as License-File in METADATA
Django-5.1.1.dist-info/LICENSEBSD-3-ClauseYes
Django-5.1.1.dist-info/LICENSE.pythonPSF License, BeOpen, CNRI, and CWI licenses, 0BSDYes
django/contrib/gis/gdal/LICENSEBSD-3-ClauseNo
django/contrib/gis/geos/LICENSEBSD-3-ClauseNo
django/dispatch/license.txtBSD-style PyDispatcher licenseNo
django/contrib/admin/static/admin/img/LICENSEMIT, Code Charm LtdNo
django/contrib/admin/static/admin/js/vendor/jquery/LICENSE.txtMIT, OpenJS FoundationNo
django/contrib/admin/static/admin/js/vendor/select2/LICENSE.mdMIT, Select2 contributorsNo
django/contrib/admin/static/admin/css/vendor/select2/LICENSE-SELECT2.mdMIT, Select2 contributorsNo
django/contrib/admin/static/admin/js/vendor/xregexp/LICENSE.txtMIT, Steven LevithanNo

The metadata points at two license files and an AUTHORS file. The wheel contains ten license files. Django's own code is BSD-3-Clause, as declared. LICENSE.python carries the license stack for code copied from the Python standard library: the PSF License, the BeOpen, CNRI, and CWI licenses, and a zero-clause BSD license for code in the Python documentation. The Django admin ships vendored jQuery, Select2, XRegExp, and an icon set, each under its own MIT license with its own copyright holder. django/dispatch was forked from PyDispatcher and keeps that project's BSD-style license. Anyone who redistributes the Django admin's static files owes those MIT notices, and none of them appear in the registry's license field.

The source distribution is different again. Django-5.1.1.tar.gz contains twelve license files, including tests/gis_tests/data/geoip2/LICENSE, which puts the GeoIP2 test data under Creative Commons Attribution-ShareAlike 3.0, a copyleft license. It also ships Font Awesome in the docs theme under CC BY 4.0, SIL OFL 1.1, and MIT. What you redistribute, a wheel, an sdist, or a vendored copy, decides which of these you owe.

npm has the same gap with a different shape. The license field in package.json is free text that the registry stores verbatim. npm's documentation defines SEE LICENSE IN <filename> for custom licenses and UNLICENSED for packages that grant no rights at all, which is the opposite of the public-domain "Unlicense". Nothing enforces SPDX. The animation library gsap publishes this:

Bash
$ npm view gsap@3.15.0 license
Standard 'no charge' license: https://gsap.com/standard-license.

The tarball for that version contains no LICENSE file. The terms live on a web page the publisher can edit after you install.

Python is fixing its half of the problem. PEP 639 adds a License-Expression field for a validated SPDX expression and a repeatable License-File field in Core Metadata 2.4, and it deprecates license classifiers. That makes declared metadata machine-readable. It does not make it complete, since vendored code inside a package still has to be found by reading the files.

How do license changes reach your dependency tree?

#

A license change reaches your dependency tree through an ordinary version upgrade. Licenses attach to versions, not to package names, and the package name usually stays the same after a relicense. The npm registry's packument for ua-parser-js shows two release lines published side by side under two licenses:

Bash
$ curl -s https://registry.npmjs.org/ua-parser-js | jq -r '.versions | to_entries[]
    | select(.key|test("^(1\\.0\\.41|2\\.0\\.0|2\\.0\\.10)$")) | "\(.key) \(.value.license)"'
2.0.0 AGPL-3.0-or-later
1.0.41 MIT
2.0.10 AGPL-3.0-or-later
$ npm view ua-parser-js dist-tags
{ next: '2.0.0', legacy: '1.0.41', latest: '2.0.10' }
MITAGPL-3.0-or-later
Date
1.x line
2.x line
2023-08-17
2.0.0-alpha.3
2023-10-05
2.0.0-beta.1license switch
2023-10-27
1.0.37
2024-11-18
2.0.0first stable 2.x
2024-12-21
1.0.40
2025-08-19
1.0.41tag: legacy
2026-05-21
2.0.10tag: latest

Same package name, two licenses, published in parallel. Dates and licenses from registry.npmjs.org, November 1, 2026.

PublishedVersionRelease lineLicenseNote
2023-08-172.0.0-alpha.32.xMITLast MIT prerelease of 2.x
2023-10-052.0.0-beta.12.xAGPL-3.0-or-laterLicense switch
2023-10-271.0.371.xMIT
2024-11-182.0.02.xAGPL-3.0-or-laterFirst stable 2.x
2024-12-211.0.401.xMIT
2025-08-191.0.411.xMITCurrent legacy tag
2026-05-212.0.102.xAGPL-3.0-or-laterCurrent latest tag

ua-parser-js changed license in the middle of its own 2.x line. Version 2.0.0-alpha.3 was MIT, and 2.0.0-beta.1, published October 5, 2023, was AGPL-3.0-or-later. Version 2.0.0 shipped on November 18, 2024, while the maintainer kept publishing MIT releases on the 0.7 and 1.x lines through August 19, 2025. As of November 2026, latest points to 2.0.10. A new npm install ua-parser-js gets the AGPL build. A project with "ua-parser-js": "^1.0.0" stays on MIT 1.0.41, because a caret range never crosses a major version.

That protection is a convention, not a rule. Semantic versioning says nothing about licenses, and a maintainer can relicense in a minor or patch release. The license change also tends to arrive as a transitive dependency that no manifest you wrote ever names, through a bot-opened upgrade PR that nobody reads for legal content.

The pattern repeats across ecosystems:

  • Fluent Assertions moved from Apache-2.0 to a paid Xceed license with version 8.0.0 on January 14, 2025, under the same NuGet package ID. Commercial use now costs $129.95 per developer per year, for a package that had more than 452 million downloads at the time.
  • Elasticsearch moved from Apache-2.0 to SSPL and the Elastic License in 2021, then later added AGPL as an option.
  • Redis went from BSD-3-Clause to RSALv2 or SSPLv1 at 7.4, then added AGPLv3 as a third option in Redis 8.0 on May 1, 2025.

Each version of each of these packages has different terms. A compliance check that runs once at adoption time is checking a package you no longer have.

How to automate open source license compliance

#

Automating open source license compliance means computing licenses from package contents on every dependency change and enforcing a policy before merge. It has three parts.

Inventory. Generate a software bill of materials with a license per component, in SPDX or CycloneDX format. CISA's 2025 draft minimum elements for SBOMs, released August 22, 2025, proposes License as a new minimum element. An SBOM built from manifests inherits every flaw of declared metadata, though. For a quick local check, npm query can select installed packages by their declared license:

Bash
$ npm install ua-parser-js express
$ npm query '[license^=AGPL]' | jq -r '.[] | "\(.name)@\(.version) \(.license) \(.location)"'
ua-parser-js@2.0.10 AGPL-3.0-or-later node_modules/ua-parser-js

In our test project, the install held 69 packages: 63 declared MIT, 4 ISC, 1 BSD-3-Clause, and 1 AGPL-3.0-or-later. The command is useful and fast, and it only reads the license field. It would report Django as BSD-3-Clause and gsap as an unparseable string.

Detection from contents. License scanning is the license half of software composition analysis. Socket's scanner reads every license file, manifest, and source header in a package, not just the registry field, and matches free-form license text against known licenses. That is how it finds the vendored licenses in Django described above. It also evaluates SPDX OR expressions across the whole dependency tree, and skips a copyleft alert when an all-permissive choice exists. Detection on free-form text is probabilistic. Some files will match a known license only partially, and some will not match anything.

Policy and enforcement. A license policy is an allow list keyed by license category, set per product shape using the table above. Socket's license policy groups licenses by Blue Oak Council tier for permissive licenses and by weak, strong, network, and maximal for copyleft. Licenses the policy does not mention are denied by default, so a newly detected license fails closed. A violation raises a License Policy Violation alert in the pull request, set to Block, Warn, or Monitor. When a human decides a finding does not apply, such as a CC-BY-SA logo file inside an otherwise ISC package, a license overlay records that decision across a version range. The overlay stops applying if the underlying license file changes in a later version, so a relicense cannot hide behind an old exception.

None of this removes the need for judgment. A tool can tell you that a GPL file is in your container image. It cannot tell you whether your use creates a derivative work. What it can do is put that question in front of a person at the pull request, while switching dependencies is still cheap, instead of during due diligence for an acquisition.

Relicensing is a supply chain event

#

npm and PyPI record a license for every version, and neither npm nor pip tells you at install time that it changed. npm prints a warning when you install a deprecated package. It prints nothing when the package you are upgrading moves from MIT to AGPL, and pip is just as quiet.

We think registries should treat a license change between versions the way npm already treats deprecation: print it at install time, show it in the upgrade diff, and reject license values that are not valid SPDX expressions. Until they do, the license of your dependency tree is a field in thousands of JSON documents that nobody diffs. Treat it like any other untrusted input, and verify it from the package contents on every change.