mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
54 lines
2.4 KiB
Python
54 lines
2.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Reject mutable container references and checked-in credential values."""
|
|
|
|
from pathlib import Path
|
|
import argparse
|
|
import re
|
|
import sys
|
|
|
|
|
|
DIGEST = re.compile(r"^[^\s@]+@sha256:[0-9a-f]{64}$")
|
|
FROM = re.compile(r"^\s*FROM(?:\s+--platform=\S+)?\s+(\S+)")
|
|
IMAGE = re.compile(r"^\s*image:\s*(\S+)\s*$")
|
|
SECRET_VALUE = re.compile(r"^\s*(?:password|token|private[-_ ]?key|publisher[-_ ]?key):\s*\S+", re.I)
|
|
|
|
|
|
def check_text(path: Path, text: str, concrete: bool) -> list[str]:
|
|
errors = []
|
|
for line_number, line in enumerate(text.splitlines(), 1):
|
|
from_match = FROM.match(line)
|
|
image_match = IMAGE.match(line)
|
|
reference = from_match.group(1) if from_match else image_match.group(1) if image_match else None
|
|
if from_match and reference:
|
|
reference = reference.split(" AS ", 1)[0].split(" as ", 1)[0]
|
|
# A bare name in a later Docker stage is an internal stage alias, not
|
|
# an independently fetched image and therefore needs no digest.
|
|
internal_stage = bool(from_match and reference and "/" not in reference and "@" not in reference and ":" not in reference)
|
|
if reference and not internal_stage and not DIGEST.fullmatch(reference):
|
|
errors.append(f"{path}:{line_number}: image is not digest-pinned: {reference}")
|
|
if concrete and reference and "@sha256:" in reference:
|
|
digest = reference.rsplit("@sha256:", 1)[1]
|
|
if set(digest) == {"0"}:
|
|
errors.append(f"{path}:{line_number}: template digest is not a release artifact")
|
|
if SECRET_VALUE.match(line):
|
|
errors.append(f"{path}:{line_number}: possible plaintext credential")
|
|
return errors
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--dockerfile", type=Path, default=Path("Dockerfile"))
|
|
parser.add_argument("--manifest-dir", type=Path, default=Path("deploy/k8s"))
|
|
parser.add_argument("--require-concrete", action="store_true")
|
|
args = parser.parse_args()
|
|
errors = check_text(args.dockerfile, args.dockerfile.read_text(), args.require_concrete)
|
|
for path in sorted(args.manifest_dir.rglob("*.y*ml")):
|
|
errors.extend(check_text(path, path.read_text(), args.require_concrete))
|
|
for error in errors:
|
|
print(error, file=sys.stderr)
|
|
return 1 if errors else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|