92 lines
3.5 KiB
Python
92 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Fail closed when a dependency-audit exception is malformed or expired."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import datetime as dt
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
EXCEPTIONS_PATH = ROOT / "security" / "pip-audit-exceptions.json"
|
|
|
|
|
|
def load_and_validate() -> tuple[dict[str, object], list[str]]:
|
|
payload = json.loads(EXCEPTIONS_PATH.read_text(encoding="utf-8"))
|
|
errors: list[str] = []
|
|
if set(payload) != {"schema_version", "advisories"}:
|
|
errors.append("exception policy must contain only schema_version and advisories")
|
|
if payload.get("schema_version") != 1:
|
|
errors.append("schema_version must be 1")
|
|
advisories = payload.get("advisories")
|
|
if not isinstance(advisories, list):
|
|
errors.append("advisories must be a list")
|
|
else:
|
|
ids = [str(item.get("id", "")) for item in advisories if isinstance(item, dict)]
|
|
if len(ids) != len(set(ids)) or any(not item.startswith("PYSEC-") for item in ids):
|
|
errors.append("advisory IDs must be unique PYSEC identifiers")
|
|
for item in advisories:
|
|
if not isinstance(item, dict):
|
|
errors.append("every advisory must be an object")
|
|
continue
|
|
required = {"id", "package", "review_by", "reason"}
|
|
allowed = required | {"aliases"}
|
|
if not required.issubset(item) or not set(item).issubset(allowed):
|
|
errors.append("every advisory must match the documented exception schema")
|
|
if not str(item.get("package", "")).strip():
|
|
errors.append("every advisory requires a package")
|
|
if len(str(item.get("reason", ""))) < 30:
|
|
errors.append("every advisory requires a specific reason")
|
|
try:
|
|
review_by = dt.date.fromisoformat(str(item["review_by"]))
|
|
except (KeyError, ValueError):
|
|
errors.append("every advisory review_by must be an ISO date")
|
|
else:
|
|
if review_by < dt.date.today():
|
|
errors.append(
|
|
f"dependency exception {item.get('id', '')} expired on "
|
|
f"{review_by.isoformat()}"
|
|
)
|
|
aliases = [
|
|
str(alias)
|
|
for item in advisories
|
|
if isinstance(item, dict)
|
|
for alias in item.get("aliases", [])
|
|
]
|
|
if len(aliases) != len(set(aliases)) or any(
|
|
not alias.startswith("CVE-") for alias in aliases
|
|
):
|
|
errors.append("container aliases must be unique CVE identifiers")
|
|
return payload, errors
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--print-ids", action="store_true")
|
|
parser.add_argument("--print-container-ids", action="store_true")
|
|
args = parser.parse_args()
|
|
payload, errors = load_and_validate()
|
|
if errors:
|
|
for error in errors:
|
|
print(f"ERROR: {error}", file=sys.stderr)
|
|
return 1
|
|
if args.print_ids:
|
|
for item in payload["advisories"]:
|
|
print(item["id"])
|
|
elif args.print_container_ids:
|
|
for item in payload["advisories"]:
|
|
for alias in item.get("aliases", []):
|
|
print(alias)
|
|
else:
|
|
print(
|
|
f"Dependency exception policy valid; {len(payload['advisories'])} active exception(s)."
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|