69 lines
2.7 KiB
Python
Executable File
69 lines
2.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Validate external artifact provenance before it enters a build context."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
from urllib.parse import urlparse
|
|
|
|
|
|
SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
|
KINDS = {"official", "china-mirror", "local-cache"}
|
|
|
|
|
|
def validate(path: Path) -> list[str]:
|
|
errors: list[str] = []
|
|
seen: set[tuple[str, str]] = set()
|
|
with path.open("r", encoding="utf-8") as handle:
|
|
for line_number, line in enumerate(handle, 1):
|
|
if not line.strip():
|
|
continue
|
|
try:
|
|
row = json.loads(line)
|
|
except json.JSONDecodeError as exc:
|
|
errors.append(f"{path}:{line_number}: invalid JSON: {exc.msg}")
|
|
continue
|
|
if row.get("record_type") != "artifact":
|
|
continue
|
|
required = {"name", "version", "source_url", "sha256", "bytes", "license", "mirror_kind"}
|
|
missing = sorted(required - row.keys())
|
|
if missing:
|
|
errors.append(f"{path}:{line_number}: missing {missing}")
|
|
continue
|
|
key = (str(row["name"]), str(row["version"]))
|
|
if key in seen:
|
|
errors.append(f"{path}:{line_number}: duplicate artifact {key[0]}@{key[1]}")
|
|
seen.add(key)
|
|
if not SHA256.fullmatch(str(row["sha256"])):
|
|
errors.append(f"{path}:{line_number}: sha256 must be 64 lowercase hex characters")
|
|
if not isinstance(row["bytes"], int) or row["bytes"] <= 0:
|
|
errors.append(f"{path}:{line_number}: bytes must be a positive integer")
|
|
if row["mirror_kind"] not in KINDS:
|
|
errors.append(f"{path}:{line_number}: invalid mirror_kind")
|
|
parsed = urlparse(str(row["source_url"]))
|
|
if row["mirror_kind"] != "local-cache" and parsed.scheme not in {"http", "https"}:
|
|
errors.append(f"{path}:{line_number}: source_url must be HTTP(S) for downloaded artifacts")
|
|
if row["mirror_kind"] == "china-mirror" and not row.get("origin_url"):
|
|
errors.append(f"{path}:{line_number}: china-mirror requires origin_url")
|
|
return errors
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--index", type=Path, default=Path("vendor/downloads/index.jsonl"))
|
|
args = parser.parse_args()
|
|
errors = validate(args.index)
|
|
if errors:
|
|
for error in errors:
|
|
print(f"ERROR: {error}")
|
|
return 1
|
|
print(f"download_index=valid path={args.index}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|