Ctrl K
ComunidadGuías…

scripts/check-transitive-licences.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3
"""Assert every transitive dependency licence is in the AGPL allowlist.

Parallels scripts/check-broker-sdk-licences.py but covers the full transitive set
(non-broker deps too). Reads supply-chain/transitive-licences.json — produced in CI by
`python -m pip_licenses --from=mixed --order=license --format=json --output-file=...` —
and checks every dep against supply-chain/licence-allowlist.yml.

Unlike the broker-SDK gate this job is INFORMATIONAL (continue-on-error in CI): a single
transitive dep relicensing typically needs a phased response (file upstream issue, swap
the dep) rather than an immediate hard block. The script still returns 1 on disallowed
licences so the signal is visible; the workflow decides whether it blocks.

Sub-spec §7.5.
"""

from __future__ import annotations

import json
import pathlib
import re
import sys

import yaml

REPO = pathlib.Path(__file__).resolve().parents[1]
ALLOWLIST_FILE = REPO / "supply-chain" / "licence-allowlist.yml"
REPORT = REPO / "supply-chain" / "transitive-licences.json"

# Common non-SPDX free-text licence strings pip_licenses emits, mapped to SPDX.
_NORMALISE = {
    "MIT License": "MIT",
    "MIT license": "MIT",
    "BSD License": "BSD-3-Clause",
    "BSD": "BSD-3-Clause",
    "Apache Software License": "Apache-2.0",
    "Apache 2.0": "Apache-2.0",
    "Apache License 2.0": "Apache-2.0",
    "The Apache Software License, Version 2.0": "Apache-2.0",
    "Mozilla Public License 2.0 (MPL 2.0)": "MPL-2.0",
    "ISC License (ISCL)": "ISC",
    "Python Software Foundation License": "Python-2.0",
}


def _split_licences(raw: str) -> list[str]:
    """pip_licenses may emit 'MIT; BSD' or 'Apache-2.0 OR MIT'."""
    parts = re.split(r"\s*(?:;|\bOR\b|\bor\b|/)\s*", (raw or "").strip())
    return [p.strip() for p in parts if p.strip()]


def main() -> int:
    if not REPORT.exists():
        print(f"NOTE: {REPORT.name} not generated yet; skipping transitive sweep")
        return 0
    allow = yaml.safe_load(ALLOWLIST_FILE.read_text(encoding="utf-8")) or {}
    allowed = set(allow.get("allowlist") or [])
    deny = set(allow.get("denylist") or [])
    review = set(allow.get("classification_review") or [])

    data = json.loads(REPORT.read_text(encoding="utf-8"))
    fails: list[str] = []
    reviews: list[str] = []
    for entry in data:
        name = entry.get("Name", "?")
        version = entry.get("Version", "?")
        raw = entry.get("License", "")
        candidates = _split_licences(raw) or [""]
        normalised = [_NORMALISE.get(c, c) for c in candidates]
        # a dep is OK if ANY of its declared licences is allowed (dual-licensed deps)
        if any(c in allowed for c in normalised):
            continue
        if any(c in deny for c in normalised):
            fails.append(f"{name} {version}: FORBIDDEN licence {raw!r}")
            continue
        if any((c in review or c == "") for c in normalised):
            reviews.append(f"{name} {version}: {raw!r} (needs classification)")
            continue
        fails.append(f"{name} {version}: licence {raw!r} not in allowlist")

    for r in reviews:
        print(f"REVIEW: {r}")
    if fails:
        for f in fails:
            print(f"FAIL: {f}", file=sys.stderr)
        return 1
    print(f"transitive licences OK ({len(data)} deps checked, {len(reviews)} need review)")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())