hardening: verify immutable supply chain references

This commit is contained in:
2026-08-26 22:12:39 +02:00
parent 86675faf49
commit 6ac87a0504
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
from __future__ import annotations
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
ACTION_SHA_RE = re.compile(
r"^\s*(?:-\s*)?uses:\s*([A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_./-]+)?)@([0-9a-f]{40})\s*(?:#.*)?$"
)
ACTION_USES_RE = re.compile(r"^\s*(?:-\s*)?uses:\s*(\S+)")
DOCKER_FROM_RE = re.compile(r"^\s*FROM\s+(?:--platform=\S+\s+)?(\S+)", re.IGNORECASE)
COMPOSE_IMAGE_RE = re.compile(r"^\s*image:\s*[\"']?([^\"'\s]+)")
def _workflow_files() -> list[Path]:
files: list[Path] = []
for root in (ROOT / ".gitea/workflows", ROOT / ".github/workflows"):
if root.is_dir():
files.extend(sorted(root.glob("*.yml")))
files.extend(sorted(root.glob("*.yaml")))
return files
def _check_workflows(violations: list[str]) -> None:
for path in _workflow_files():
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
match = ACTION_USES_RE.match(line)
if not match:
continue
value = match.group(1)
if value.startswith("./"):
continue
if value.startswith("docker://"):
image = value.removeprefix("docker://")
if "@sha256:" not in image:
violations.append(
f"{path.relative_to(ROOT)}:{number}: docker action is not digest-pinned"
)
continue
if not ACTION_SHA_RE.match(line):
violations.append(
f"{path.relative_to(ROOT)}:{number}: action must use a full 40-char commit SHA: {value}"
)
def _check_dockerfiles(violations: list[str]) -> None:
dockerfiles = (
ROOT / "backend/Dockerfile",
ROOT / "frontend/Dockerfile",
ROOT / "deploy/unraid/Dockerfile.all-in-one",
)
for path in dockerfiles:
if not path.is_file():
continue
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
match = DOCKER_FROM_RE.match(line)
if not match:
continue
image = match.group(1)
if image == "scratch":
continue
if "${" in image:
violations.append(
f"{path.relative_to(ROOT)}:{number}: dynamic external base image is not allowed"
)
continue
if "@sha256:" not in image:
violations.append(
f"{path.relative_to(ROOT)}:{number}: base image must be digest-pinned: {image}"
)
def _check_compose(violations: list[str]) -> None:
for relative in ("docker-compose.yml", "docker-compose.unraid.yml"):
path = ROOT / relative
if not path.is_file():
continue
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
match = COMPOSE_IMAGE_RE.match(line)
if not match:
continue
image = match.group(1)
if image.startswith(("geointel-", "${")):
continue
if "@sha256:" not in image:
violations.append(
f"{relative}:{number}: external image must be digest-pinned: {image}"
)
def main() -> int:
violations: list[str] = []
_check_workflows(violations)
_check_dockerfiles(violations)
_check_compose(violations)
if violations:
print("Supply-chain pin violations:", file=sys.stderr)
for violation in violations:
print(f" - {violation}", file=sys.stderr)
return 1
print("Supply-chain pins passed.")
return 0
if __name__ == "__main__":
raise SystemExit(main())