Agent Wranglers

OSS-42Recommended

Check every dependency licence against an allow list in CI

Keep one written licence policy with a list of allowed, reviewed and denied licences. A CI job reads the licence of every direct and indirect dependency and fails on any licence outside the allowed list.

How to do it

A strong copyleft licence can oblige you to publish your own code. The AGPL does so even when the software only runs on your server and users reach it over a network. So a SaaS product usually denies AGPL, while a project that is itself under the GPL may allow it. Read each licence as its SPDX identifier from the package metadata or the bill of materials. Treat a package with no licence, or one marked UNLICENSED, as denied. A reviewed exception names the package, the version, the reason and the person who approved it. The policy is the one Keep an open licence or a way out applies when choosing software. A due diligence review reads the same list, as List the licences of the code and every dependency describes. Choosing a new dependency by its licence is Keep an open licence or a way out.

Examples

JavaScript example

// licence-check.mjs: fails on any licence outside the allowed list
import { readFileSync } from 'node:fs';
const ALLOWED = new Set(['MIT', 'ISC', 'Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause', '0BSD']);
const exceptions = JSON.parse(readFileSync('licence-exceptions.json', 'utf8'));
const lock = JSON.parse(readFileSync('package-lock.json', 'utf8'));
const failures = Object.entries(lock.packages)
  .filter(([path]) => path.startsWith('node_modules/'))
  .map(([path, pkg]) => ({ name: path.slice(path.lastIndexOf('node_modules/') + 13), version: pkg.version, licence: pkg.license ?? 'UNLICENSED' }))
  .filter(pkg => !ALLOWED.has(pkg.licence) && !exceptions.some(e => e.name === pkg.name && e.version === pkg.version));
failures.forEach(pkg => console.log(`FAIL ${pkg.name}@${pkg.version} is ${pkg.licence}`));
process.exit(failures.length ? 1 : 0);

Python example

# licence_check.py: the same policy for an installed Python environment
import json, sys
from importlib.metadata import distributions

ALLOWED = {"MIT", "ISC", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "PSF-2.0"}
exceptions = {(e["name"], e["version"]) for e in json.load(open("licence-exceptions.json"))}
failures = []
for dist in distributions():
    licence = dist.metadata.get("License-Expression") or dist.metadata.get("License") or "UNLICENSED"
    if licence not in ALLOWED and (dist.metadata["Name"], dist.version) not in exceptions:
        failures.append(f"{dist.metadata['Name']}=={dist.version} is {licence}")
print("\n".join(f"FAIL {failure}" for failure in failures))
sys.exit(1 if failures else 0)

How to check

The licence job runs on every pull request that changes a lock file. Adding a package licensed AGPL-3.0-only on a branch fails the job, unless the exception file names that package and version.

Sources

For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/open-source/using-open-source/dependency-licence-policy">Agent Wranglers open source maintenance playbook</a>. In code you write, add a comment next to it with the same link.