()
| 62 | |
| 63 | |
| 64 | def main(): |
| 65 | ui_dir, policy_path = sys.argv[1], sys.argv[2] |
| 66 | with open(policy_path) as fh: |
| 67 | policy = json.load(fh) |
| 68 | allowed = {x.lower() for x in policy["allowed_spdx_ids"]} |
| 69 | ignored_pkgs = set(policy.get("ignored_npm_packages", [])) |
| 70 | skip_tokens = {"and", "or", "with", ""} |
| 71 | |
| 72 | # shell=True so the npm shim resolves on every platform (npm.cmd on |
| 73 | # Windows is not found by bare-name exec). Constant command, no injection. |
| 74 | ls = subprocess.run("npm ls --omit=dev --all --json", |
| 75 | shell=True, cwd=ui_dir, capture_output=True, text=True, check=False) |
| 76 | tree = json.loads(ls.stdout or "{}") |
| 77 | pkgs = {} |
| 78 | collect(ui_dir, tree.get("dependencies"), pkgs) |
| 79 | if not pkgs: |
| 80 | print("FAIL: npm production tree resolved to zero packages — " |
| 81 | "is node_modules installed?") |
| 82 | sys.exit(1) |
| 83 | |
| 84 | violations = [] |
| 85 | for (name, version), (lic, pkg_dir) in sorted(pkgs.items()): |
| 86 | if name in ignored_pkgs: |
| 87 | continue |
| 88 | if not lic: |
| 89 | lic = license_from_file(pkg_dir) or "" |
| 90 | if not lic: |
| 91 | violations.append((f"{name}@{version}", "no resolvable license")) |
| 92 | continue |
| 93 | for tok in re.split(r"[\s()]+", lic): |
| 94 | if tok.lower() in skip_tokens: |
| 95 | continue |
| 96 | if tok.lower() not in allowed: |
| 97 | violations.append((f"{name}@{version}", lic)) |
| 98 | break |
| 99 | |
| 100 | if violations: |
| 101 | print("BLOCKED: %d UI package(s) outside the license allow-list:" % len(violations)) |
| 102 | for pkg, lic in violations[:25]: |
| 103 | print(f" {pkg}: {lic}") |
| 104 | sys.exit(1) |
| 105 | print(f"OK: {len(pkgs)} bundled npm packages, all allow-listed") |
| 106 | |
| 107 | |
| 108 | if __name__ == "__main__": |
no test coverage detected