Bound advisory network retries while keeping audits fail closed
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
"""Retry transient advisory-service failures without accepting incomplete audits."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
NETWORK_ERRORS = (
|
||||
"ReadTimeout",
|
||||
"Read timed out",
|
||||
"ConnectTimeout",
|
||||
"ConnectionError",
|
||||
"ConnectionResetError",
|
||||
"NameResolutionError",
|
||||
"Temporary failure in name resolution",
|
||||
"MaxRetryError",
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
cache = Path(tempfile.gettempdir()) / "frame-flow-advisory-cache"
|
||||
cache.mkdir(mode=0o700, exist_ok=True)
|
||||
timeout = int(os.getenv("CI_AUDIT_TIMEOUT", "60"))
|
||||
attempts = int(os.getenv("CI_AUDIT_ATTEMPTS", "3"))
|
||||
if not 15 <= timeout <= 120 or not 1 <= attempts <= 3:
|
||||
raise SystemExit("Invalid audit timeout or attempt limit")
|
||||
print(
|
||||
"Advisory host: pypi.org; HTTP cache enabled; socket timeout:",
|
||||
timeout,
|
||||
flush=True,
|
||||
)
|
||||
command = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"pip_audit",
|
||||
"-r",
|
||||
"requirements.lock",
|
||||
"--no-deps",
|
||||
"--disable-pip",
|
||||
"--vulnerability-service",
|
||||
"pypi",
|
||||
"--timeout",
|
||||
str(timeout),
|
||||
"--cache-dir",
|
||||
str(cache),
|
||||
"--format",
|
||||
"json",
|
||||
]
|
||||
for attempt in range(1, attempts + 1):
|
||||
result = subprocess.run(command, capture_output=True, text=True, timeout=300)
|
||||
try:
|
||||
report = json.loads(result.stdout)
|
||||
dependencies = report["dependencies"]
|
||||
complete = bool(dependencies) and all(
|
||||
isinstance(d.get("vulns"), list) and not d.get("skip_reason")
|
||||
for d in dependencies
|
||||
)
|
||||
except (ValueError, KeyError, TypeError, AttributeError):
|
||||
dependencies, complete = [], False
|
||||
vulnerabilities = [
|
||||
(d["name"], v["id"]) for d in dependencies for v in d.get("vulns", [])
|
||||
]
|
||||
if vulnerabilities:
|
||||
for name, advisory in vulnerabilities:
|
||||
print("Vulnerable dependency:", name, advisory, flush=True)
|
||||
raise SystemExit("Audit found vulnerabilities; deployment blocked")
|
||||
if result.returncode == 0 and complete:
|
||||
print(
|
||||
"Complete audit passed:",
|
||||
len(dependencies),
|
||||
"dependencies; no known vulnerabilities",
|
||||
flush=True,
|
||||
)
|
||||
return
|
||||
transient = any(marker in result.stderr for marker in NETWORK_ERRORS)
|
||||
# Do not print raw network exceptions: proxy URLs can contain credentials.
|
||||
print(
|
||||
"Audit attempt",
|
||||
attempt,
|
||||
"failed:",
|
||||
"network error" if transient else "incomplete/error result",
|
||||
flush=True,
|
||||
)
|
||||
if not transient or attempt == attempts:
|
||||
raise SystemExit("Audit unsuccessful; deployment blocked")
|
||||
time.sleep(5 * attempt)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user