|
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())
|