This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify ZIP safety, embedded checksums and extracted build-pack validation."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path, PurePosixPath
|
||||
|
||||
|
||||
def parse_manifest(data: bytes) -> dict[str, str]:
|
||||
result: dict[str, str] = {}
|
||||
for number, line in enumerate(data.decode("utf-8").splitlines(), 1):
|
||||
if not line.strip():
|
||||
continue
|
||||
try:
|
||||
digest, relative = line.split(" ", 1)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"Invalid checksum line {number}") from exc
|
||||
if len(digest) != 64 or any(c not in "0123456789abcdef" for c in digest):
|
||||
raise ValueError(f"Invalid SHA-256 at line {number}")
|
||||
if relative in result:
|
||||
raise ValueError(f"Duplicate checksum path: {relative}")
|
||||
result[relative] = digest
|
||||
return result
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("archive", type=Path)
|
||||
args = parser.parse_args()
|
||||
archive_path = args.archive.resolve()
|
||||
if not archive_path.is_file():
|
||||
raise SystemExit(f"Archive not found: {archive_path}")
|
||||
|
||||
with zipfile.ZipFile(archive_path) as archive:
|
||||
infos = archive.infolist()
|
||||
names = [info.filename for info in infos]
|
||||
if len(names) != len(set(names)):
|
||||
raise SystemExit("Archive contains duplicate paths")
|
||||
for info in infos:
|
||||
path = PurePosixPath(info.filename)
|
||||
if path.is_absolute() or ".." in path.parts or not path.parts:
|
||||
raise SystemExit(f"Unsafe archive path: {info.filename}")
|
||||
mode = (info.external_attr >> 16) & 0xFFFF
|
||||
if stat.S_ISLNK(mode):
|
||||
raise SystemExit(f"Archive contains symlink: {info.filename}")
|
||||
bad = archive.testzip()
|
||||
if bad:
|
||||
raise SystemExit(f"CRC failure: {bad}")
|
||||
|
||||
manifest_names = [n for n in names if n.endswith("/PACK_MANIFEST.sha256")]
|
||||
index_names = [n for n in names if n.endswith("/FILE_INDEX.txt")]
|
||||
if len(manifest_names) != 1 or len(index_names) != 1:
|
||||
raise SystemExit("Archive must contain one checksum manifest and one file index")
|
||||
manifest_name = manifest_names[0]
|
||||
prefix = manifest_name[: -len("PACK_MANIFEST.sha256")]
|
||||
if index_names[0] != prefix + "FILE_INDEX.txt":
|
||||
raise SystemExit("Checksum manifest and file index do not share one root")
|
||||
|
||||
manifest = parse_manifest(archive.read(manifest_name))
|
||||
index = {line for line in archive.read(index_names[0]).decode("utf-8").splitlines() if line}
|
||||
archived_relative = {n[len(prefix):] for n in names if n.startswith(prefix) and not n.endswith("/")}
|
||||
if index != archived_relative:
|
||||
missing = sorted(index - archived_relative)
|
||||
extra = sorted(archived_relative - index)
|
||||
raise SystemExit(f"FILE_INDEX mismatch; missing={missing}, extra={extra}")
|
||||
expected_manifest_paths = archived_relative - {"PACK_MANIFEST.sha256"}
|
||||
if set(manifest) != expected_manifest_paths:
|
||||
raise SystemExit("Checksum manifest path set does not match archive")
|
||||
for relative, expected in manifest.items():
|
||||
actual = hashlib.sha256(archive.read(prefix + relative)).hexdigest()
|
||||
if actual != expected:
|
||||
raise SystemExit(f"Checksum mismatch: {relative}")
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="devrunbook-verify-") as temp:
|
||||
archive.extractall(temp)
|
||||
root = Path(temp) / PurePosixPath(prefix).parts[0]
|
||||
subprocess.run([sys.executable, str(root / "scripts/validate_pack.py")], cwd=root, check=True)
|
||||
|
||||
print("DevRunbook archive verification PASSED")
|
||||
print(f"- Entries: {len(names)}")
|
||||
print("- Safe paths, no duplicates, no symlinks and valid CRCs")
|
||||
print("- Embedded file index and SHA-256 manifest verified")
|
||||
print("- Extracted build-pack validator passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user