vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys
keenan · 50m ago
4b8c9e1d
| 1 | #!/usr/bin/env python3 |
| 2 | """Fail when something this repository ships may not be shipped. |
| 3 | |
| 4 | `vole workflow run verify` and the release run build a bill of materials first, |
| 5 | then call this: |
| 6 | |
| 7 | python3 scripts/check-licenses.py bom.json |
| 8 | |
| 9 | Two inventories are checked, because they arrive by different routes. |
| 10 | |
| 11 | Go modules have metadata, so the scanner reports a license for each one and this |
| 12 | script compares it against `licenses.toml`. A module with no detected license is a |
| 13 | failure, not a pass: unknown is not permission. |
| 14 | |
| 15 | Vendored browser assets have no metadata at all, so `NOTICE` is their inventory. |
| 16 | Every license file beside an asset must be named there, and every license file |
| 17 | `NOTICE` names must exist. Checking one direction only lets a new asset in |
| 18 | unnoticed. |
| 19 | """ |
| 20 | |
| 21 | import json |
| 22 | import os |
| 23 | import re |
| 24 | import sys |
| 25 | import tomllib |
| 26 | |
| 27 | ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| 28 | STATIC = os.path.join(ROOT, "server", "web", "static") |
| 29 | ALLOWLIST = os.path.join(ROOT, "licenses.toml") |
| 30 | NOTICE = os.path.join(ROOT, "NOTICE") |
| 31 | |
| 32 | # A license file a vendored asset carries beside it: htmx.LICENSE, prism.LICENSE, |
| 33 | # and the fonts' OFL-*.txt. |
| 34 | LICENSE_FILE = re.compile(r"(?:^|/)[\w.-]*LICENSE[\w.-]*$|(?:^|/)OFL-[\w.-]+\.txt$") |
| 35 | |
| 36 | |
| 37 | def module_findings(bom: dict) -> tuple[list[str], list[str]]: |
| 38 | """Compare every linked module's license against the allowlist.""" |
| 39 | with open(ALLOWLIST, encoding="utf-8") as f: |
| 40 | allowed = tomllib.loads(f.read())["modules"] |
| 41 | findings, seen = [], [] |
| 42 | for component in bom.get("components", []): |
| 43 | name = component.get("name") or "?" |
| 44 | # The standard library ships with the toolchain. Its license is the |
| 45 | # toolchain's, not a dependency this project chose. |
| 46 | if name == "std": |
| 47 | continue |
| 48 | licenses = (component.get("evidence") or {}).get("licenses") or component.get("licenses") or [] |
| 49 | if not licenses: |
| 50 | findings.append(f"{name}: no license detected, so it cannot be allowed") |
| 51 | continue |
| 52 | for entry in licenses: |
| 53 | # A compound license arrives as an expression rather than an id; it |
| 54 | # has to be allowed as a whole, because reading "MIT OR GPL-3.0" as |
| 55 | # MIT would be exactly the mistake this check exists to catch. |
| 56 | identifier = entry.get("expression") or (entry.get("license") or {}).get("id") |
| 57 | if not identifier: |
| 58 | findings.append(f"{name}: a license entry this check cannot read") |
| 59 | continue |
| 60 | seen.append(identifier) |
| 61 | if identifier not in allowed: |
| 62 | findings.append(f"{name}: {identifier} is not in licenses.toml") |
| 63 | return findings, sorted(set(seen)) |
| 64 | |
| 65 | |
| 66 | def asset_findings() -> list[str]: |
| 67 | """Check NOTICE against the license files beside the vendored assets.""" |
| 68 | with open(NOTICE, encoding="utf-8") as f: |
| 69 | notice = f.read() |
| 70 | findings = [] |
| 71 | for root, dirs, files in os.walk(STATIC): |
| 72 | dirs.sort() |
| 73 | for name in sorted(files): |
| 74 | path = os.path.join(root, name) |
| 75 | relative = os.path.relpath(path, ROOT).replace(os.sep, "/") |
| 76 | if not LICENSE_FILE.search(relative): |
| 77 | continue |
| 78 | if relative not in notice: |
| 79 | findings.append(f"{relative}: not named in NOTICE") |
| 80 | # The suffix stops at the identifier, so the full stop that ends the sentence |
| 81 | # in NOTICE is not read as part of the path. |
| 82 | named = set(re.findall(r"[\w./-]*?(?:LICENSE[\w-]*|OFL-[\w-]+\.txt)", notice)) |
| 83 | for path in sorted(named): |
| 84 | if not os.path.isfile(os.path.join(ROOT, path)): |
| 85 | findings.append(f"NOTICE names {path}, which does not exist") |
| 86 | return findings |
| 87 | |
| 88 | |
| 89 | def main(argv: list[str]) -> int: |
| 90 | if len(argv) != 2: |
| 91 | print("usage: check-licenses.py <sbom.json>", file=sys.stderr) |
| 92 | return 2 |
| 93 | try: |
| 94 | with open(argv[1], encoding="utf-8") as f: |
| 95 | bom = json.load(f) |
| 96 | except OSError as err: |
| 97 | print(f"{argv[1]}: {err.strerror}; generate it with cyclonedx-gomod first", file=sys.stderr) |
| 98 | return 2 |
| 99 | except json.JSONDecodeError as err: |
| 100 | print(f"{argv[1]}: not a bill of materials: {err}", file=sys.stderr) |
| 101 | return 2 |
| 102 | |
| 103 | findings, seen = module_findings(bom) |
| 104 | findings += asset_findings() |
| 105 | if findings: |
| 106 | print("\n".join(findings)) |
| 107 | return 1 |
| 108 | modules = [c for c in bom.get("components", []) if c.get("name") != "std"] |
| 109 | print(f"{len(modules)} modules ({', '.join(seen)}); NOTICE matches the assets") |
| 110 | return 0 |
| 111 | |
| 112 | |
| 113 | if __name__ == "__main__": |
| 114 | sys.exit(main(sys.argv)) |
| 115 |