hardening: verify immutable supply chain references
This commit is contained in:
@@ -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())
|
||||||
Reference in New Issue
Block a user