Blame SOURCES/check-null-licenses

rdobuilder 30d276
#!/usr/bin/python3
rdobuilder 30d276
# -*- coding: utf-8 -*-
rdobuilder 30d276
rdobuilder 30d276
import json
rdobuilder 30d276
from argparse import ArgumentParser, FileType, RawDescriptionHelpFormatter
rdobuilder 30d276
from pathlib import Path
rdobuilder 30d276
from sys import exit, stderr
rdobuilder 30d276
rdobuilder 30d276
try:
rdobuilder 30d276
    import tomllib
rdobuilder 30d276
except ImportError:
rdobuilder 30d276
    import tomli as tomllib
rdobuilder 30d276
rdobuilder 30d276
rdobuilder 30d276
def main():
rdobuilder 30d276
    args = parse_args()
rdobuilder 30d276
    problem = False
rdobuilder 30d276
    if not args.tree.is_dir():
rdobuilder 30d276
        return f"Not a directory: {args.tree}"
rdobuilder 30d276
    for pjpath in args.tree.glob("**/package.json"):
rdobuilder 30d276
        name, version, license = parse(pjpath)
rdobuilder 30d276
        identity = f"{name} {version}"
rdobuilder 30d276
        if version in args.exceptions.get(name, ()):
rdobuilder 30d276
            continue  # Do not even check the license
rdobuilder 30d276
        elif license is None:
rdobuilder 30d276
            problem = True
rdobuilder 30d276
            print(f"Missing license in package.json for {identity}", file=stderr)
rdobuilder 30d276
        elif isinstance(license, dict):
rdobuilder 30d276
            if isinstance(license.get("type"), str):
rdobuilder 30d276
                continue
rdobuilder 30d276
            print(
rdobuilder 30d276
                (
rdobuilder 30d276
                    "Missing type for (deprecated) license object in "
rdobuilder 30d276
                    f"package.json for {identity}: {license}"
rdobuilder 30d276
                ),
rdobuilder 30d276
                file=stderr,
rdobuilder 30d276
            )
rdobuilder 30d276
        elif isinstance(license, list):
rdobuilder 30d276
            if license and all(
rdobuilder 30d276
                isinstance(entry, dict) and isinstance(entry.get("type"), str)
rdobuilder 30d276
                for entry in license
rdobuilder 30d276
            ):
rdobuilder 30d276
                continue
rdobuilder 30d276
            print(
rdobuilder 30d276
                (
rdobuilder 30d276
                    "Defective (deprecated) licenses array-of objects in "
rdobuilder 30d276
                    f"package.json for {identity}: {license}"
rdobuilder 30d276
                ),
rdobuilder 30d276
                file=stderr,
rdobuilder 30d276
            )
rdobuilder 30d276
        elif isinstance(license, str):
rdobuilder 30d276
            continue
rdobuilder 30d276
        else:
rdobuilder 30d276
            print(
rdobuilder 30d276
                (
rdobuilder 30d276
                    "Weird type for license in "
rdobuilder 30d276
                    f"package.json for {identity}: {license}"
rdobuilder 30d276
                ),
rdobuilder 30d276
                file=stderr,
rdobuilder 30d276
            )
rdobuilder 30d276
        problem = True
rdobuilder 30d276
    if problem:
rdobuilder 30d276
        return "At least one missing license was found."
rdobuilder 30d276
rdobuilder 30d276
rdobuilder 30d276
def parse(package_json_path):
rdobuilder 30d276
    with package_json_path.open("rb") as pjfile:
rdobuilder 30d276
        pj = json.load(pjfile)
rdobuilder 30d276
    try:
rdobuilder 30d276
        license = pj["license"]
rdobuilder 30d276
    except KeyError:
rdobuilder 30d276
        license = pj.get("licenses")
rdobuilder 30d276
    try:
rdobuilder 30d276
        name = pj["name"]
rdobuilder 30d276
    except KeyError:
rdobuilder 30d276
        name = package_json_path.parent.name
rdobuilder 30d276
    version = pj.get("version", "<unknown version>")
rdobuilder 30d276
rdobuilder 30d276
    return name, version, license
rdobuilder 30d276
rdobuilder 30d276
rdobuilder 30d276
def parse_args():
rdobuilder 30d276
    parser = ArgumentParser(
rdobuilder 30d276
        formatter_class=RawDescriptionHelpFormatter,
rdobuilder 30d276
        description=("Search for bundled dependencies without declared licenses"),
rdobuilder 30d276
        epilog="""
rdobuilder 30d276
rdobuilder 30d276
The exceptions file must be a TOML file with zero or more tables. Each table’s
rdobuilder 30d276
keys are package names; the corresponding values values are exact version
rdobuilder 30d276
number strings, or arrays of version number strings, that have been manually
rdobuilder 30d276
audited to determine their license status and should therefore be ignored.
rdobuilder 30d276
rdobuilder 30d276
Exceptions in a table called “any” are always applied. Otherwise, exceptions
rdobuilder 30d276
are applied only if a corresponding --with TABLENAME argument is given;
rdobuilder 30d276
multiple such arguments may be given.
rdobuilder 30d276
rdobuilder 30d276
For
rdobuilder 30d276
example:
rdobuilder 30d276
rdobuilder 30d276
    [any]
rdobuilder 30d276
    example-foo = "1.0.0"
rdobuilder 30d276
rdobuilder 30d276
    [prod]
rdobuilder 30d276
    example-bar = [ "2.0.0", "2.0.1",]
rdobuilder 30d276
rdobuilder 30d276
    [dev]
rdobuilder 30d276
    example-bat = [ "3.7.4",]
rdobuilder 30d276
rdobuilder 30d276
would always ignore version 1.0.0 of example-foo. It would ignore example-bar
rdobuilder 30d276
2.0.1 only when called with “--with prod”.
rdobuilder 30d276
rdobuilder 30d276
Comments may (and should) be used to describe the manual audits upon which the
rdobuilder 30d276
exclusions are based.
rdobuilder 30d276
rdobuilder 30d276
Otherwise, any package.json with missing or null license field in the tree is
rdobuilder 30d276
considered an error, and the program returns with nonzero status.
rdobuilder 30d276
""",
rdobuilder 30d276
    )
rdobuilder 30d276
    parser.add_argument(
rdobuilder 30d276
        "-x",
rdobuilder 30d276
        "--exceptions",
rdobuilder 30d276
        type=FileType("rb"),
rdobuilder 30d276
        help="Manually audited package versions file",
rdobuilder 30d276
    )
rdobuilder 30d276
    parser.add_argument(
rdobuilder 30d276
        "-w",
rdobuilder 30d276
        "--with",
rdobuilder 30d276
        action="append",
rdobuilder 30d276
        default=[],
rdobuilder 30d276
        help="Enable a table in the exceptions file",
rdobuilder 30d276
    )
rdobuilder 30d276
    parser.add_argument(
rdobuilder 30d276
        "tree",
rdobuilder 30d276
        metavar="node_modules_dir",
rdobuilder 30d276
        type=Path,
rdobuilder 30d276
        help="Path to search recursively",
rdobuilder 30d276
        default=".",
rdobuilder 30d276
    )
rdobuilder 30d276
    args = parser.parse_args()
rdobuilder 30d276
rdobuilder 30d276
    if args.exceptions is None:
rdobuilder 30d276
        args.exceptions = {}
rdobuilder 30d276
        xname = None
rdobuilder 30d276
    else:
rdobuilder 30d276
        with args.exceptions as xfile:
rdobuilder 30d276
            xname = getattr(xfile, "name", "<exceptions>")
rdobuilder 30d276
            args.exceptions = tomllib.load(args.exceptions)
rdobuilder 30d276
        if not isinstance(args.exceptions, dict):
rdobuilder 30d276
            parser.error(f"Invalid format in {xname}: not an object")
rdobuilder 30d276
        for tablename, table in args.exceptions.items():
rdobuilder 30d276
            if not isinstance(table, dict):
rdobuilder 30d276
                parser.error(f"Non-table entry in {xname}: {tablename} = {table!r}")
rdobuilder 30d276
            overlay = {}
rdobuilder 30d276
            for key, value in table.items():
rdobuilder 30d276
                if isinstance(value, str):
rdobuilder 30d276
                    overlay[key] = [value]
rdobuilder 30d276
                elif not isinstance(value, list) or not all(
rdobuilder 30d276
                    isinstance(entry, str) for entry in value
rdobuilder 30d276
                ):
rdobuilder 30d276
                    parser.error(
rdobuilder 30d276
                        f"Invalid format in {xname} in [{tablename}]: "
rdobuilder 30d276
                        f"{key!r} = {value!r}"
rdobuilder 30d276
                    )
rdobuilder 30d276
            table.update(overlay)
rdobuilder 30d276
rdobuilder 30d276
    x = args.exceptions.get("any", {})
rdobuilder 30d276
    for add in getattr(args, "with"):
rdobuilder 30d276
        try:
rdobuilder 30d276
            x.update(args.exceptions[add])
rdobuilder 30d276
        except KeyError:
rdobuilder 30d276
            if xname is None:
rdobuilder 30d276
                parser.error(f"No table {add}, as no exceptions file was given")
rdobuilder 30d276
            else:
rdobuilder 30d276
                parser.error(f"No table {add} in {xname}")
rdobuilder 30d276
    # Store the merged dictionary
rdobuilder 30d276
    args.exceptions = x
rdobuilder 30d276
rdobuilder 30d276
    return args
rdobuilder 30d276
rdobuilder 30d276
rdobuilder 30d276
if __name__ == "__main__":
rdobuilder 30d276
    exit(main())