| name | audit-license-compliance |
| description | Audits open-source license compliance — resolves SPDX identifiers across the full transitive dependency tree (license-checker/scancode), classifies copyleft (GPL/AGPL/LGPL) exposure against the distribution model, enforces an allow/deny CI policy, and generates NOTICE/THIRD-PARTY attribution files. |
| when_to_use | Shipping a product/library or prepping a legal/procurement review and needing to clear OSS license obligations. Distinct from supply-chain-sbom-provenance (build integrity, SBOM signing, provenance), dependency-upgrade (version bumps), and publish-package-registry (the publish step). |
When to Use
Reach for this skill when the question is about license obligations, not which versions to ship or whether the build is tamper-proof:
- "Can we ship — does anything here have a GPL/AGPL problem?"
- "Generate the NOTICE / THIRD-PARTY-LICENSES file for the release."
- "Add a CI gate that fails the build on a forbidden license."
- "Legal/procurement needs the full list of dependencies and their licenses."
- "This transitive dep has no license / is dual-licensed — what do we do?"
- "We're going from internal SaaS to a downloadable binary — what changes?"
NOT this skill:
- Generating/signing an SBOM, provenance, or attestations → supply-chain-sbom-provenance (it lists components; this skill judges their licenses)
- Bumping a version or swapping a GPL dep for an alternative → dependency-upgrade
- The actual
npm publish / twine upload step and its OIDC gate → publish-package-registry
- A design-level risk enumeration of the system → threat-model-stride
Steps
-
Scan the FULL transitive tree, not just direct deps — and pin the result to a manifest. Direct deps are a tiny minority; copyleft almost always rides in transitively. Pick the resolver for the ecosystem and emit machine-readable output:
npx license-checker-rseki --production --json --out licenses.json
pip-licenses --format=json --with-license-file --with-urls > licenses.json
cargo install cargo-deny && cargo deny list -f json > licenses.json
go install github.com/google/go-licenses@latest && go-licenses report ./... > licenses.csv
pipx run scancode-toolkit scancode --license --json-pp scancode.json <vendored_dir>
Scope to what you distribute: prod/runtime deps only. devDependencies, test, and build-only tooling are generally not distributed — exclude them (--production, --omit=dev) or you'll drown in false GPL hits from linters. When package metadata is missing or wrong, scancode reading the real LICENSE/headers is the tiebreaker, not the package.json license field.
-
Classify each license by risk against YOUR distribution model — this is the whole audit. The same license is fine or fatal depending on how you ship. Decide the model first, then read the table left-to-right:
| License class | Examples | SaaS (network only) | Distributed binary / app | Library you publish |
|---|
| Permissive | MIT, BSD-2/3, ISC, Apache-2.0, Unlicense, 0BSD | ✅ allow | ✅ allow (must keep NOTICE) | ✅ allow |
| Weak copyleft (file) | MPL-2.0, EPL-2.0, CDDL | ✅ allow | ⚠️ allow if unmodified & file-isolated | ⚠️ review |
| Weak copyleft (lib) | LGPL-2.1/3.0 | ✅ allow | ⚠️ dynamic link only; static link triggers relink obligation | ⚠️ review |
| Strong copyleft | GPL-2.0, GPL-3.0 | ✅ allow (no conveying) | ❌ deny — forces whole-program source disclosure |
Common Errors
- Scanning direct deps only. The GPL/AGPL almost always arrives 3 levels deep. Always resolve the full transitive tree (
--production flattens it).
- Including devDependencies in the distributed verdict. A GPL linter or test runner isn't distributed and isn't a violation — it just floods the report and gets the gate disabled. Scope to runtime/prod.
- Trusting the package
license field over the actual files. Metadata is frequently wrong, stale, or SEE LICENSE IN .... When it matters, let scancode read the real LICENSE/headers; that's ground truth.
- Treating AGPL like GPL for a SaaS. AGPL's network clause means serving it over HTTP is conveying — source must be offered to every user. Deny AGPL even when GPL would be fine for your server-only model.
- Static-linking an LGPL library into a shipped binary. That triggers the relink obligation (users must be able to swap the lib). Dynamic-link it, or treat it as deny for static builds.
- Auto-picking a side of a dual license silently.
(GPL-2.0 OR MIT) is only safe because you elect MIT — record the election. If the tool defaulted to GPL, you may have manufactured an obligation that didn't exist.
- "No license" read as permissive. Absence of a license = all rights reserved = you have no grant to use it. Default-deny and resolve, don't ship.
- Allowlist that fails open on unknowns. A typo SPDX id or low-confidence match must fail the build, not pass. Set
confidence-threshold and deny-by-default.
- Hand-maintaining NOTICE. It drifts the moment a transitive dep changes. Generate it from the lockfile in CI and diff against the committed copy.
- Confusing source-available with open-source. SSPL/BUSL/Elastic-2.0/"Commons Clause" are usage-restricted and not OSI-approved — deny them unless legal explicitly cleared the specific use.
Verify
- Coverage: The scan output lists transitive deps, not just the handful in
package.json/Cargo.toml. Spot-check a known deep dep appears with a license.
- Policy gate fires (positive control): Add a dep with a denied license (e.g. a GPL-3.0 package) on a throwaway branch → CI fails with a message naming the dep and its license. Revert.
- Unknown fails closed: Point the scanner at a dep with a stripped/garbled license → it is reported as unknown and the gate fails, not passes.
- Distribution-model correctness: Re-run classification under the other model (flip SaaS↔distributed) and confirm AGPL/LGPL/GPL verdicts change as the table predicts — proves the model is actually driving the verdict, not hardcoded.
- Attribution completeness: Every distributed (prod) dependency in the lockfile appears in
THIRD-PARTY-LICENSES/NOTICE with its license text. Count of attributed deps == count of distributed deps; no entry is empty.
- Attribution drift gate: Add a prod dep without regenerating → CI's NOTICE-diff check fails. Regenerate → it passes and the new dep+license is present.
- Dual/missing resolved: No dep is left with an unresolved
OR expression or empty license; each exception in the policy has a written rationale.
Done = the scan covers the full transitive prod tree, CI blocks a newly added denied-license and an unknown-license dep (verified by positive controls), every distributed dependency is listed with its license in the committed NOTICE, and the NOTICE-drift gate fails when that list goes stale.