"""Download gitleaks and hadolint binaries into a local bin dir on import.""" import os import platform import shutil import sys import tarfile import zipfile from pathlib import Path from urllib.parse import urlparse import requests from .helpers import have_binary GITLEAKS_VERSION = "8.18.4" HADOLINT_VERSION = "2.12.0" _IS_WINDOWS = platform.system() == "Windows" # Allowlist: only these domains may be used for binary downloads. _ALLOWED_DOWNLOAD_DOMAINS = frozenset({"github.com", "objects.githubusercontent.com"}) # On Windows put binaries next to the Python executable (venv Scripts/) # so shutil.which() finds them without PATH manipulation. # On Linux/macOS use ~/.local/bin as before. if _IS_WINDOWS: BIN_DIR = Path(sys.executable).parent # .venv\Scripts\ else: # pragma: no cover BIN_DIR = Path.home() / ".local" / "bin" def _stream_download(url: str, dest_path: Path, timeout: int = 120) -> None: """Download *url* to *dest_path*. Only HTTPS from approved domains is allowed.""" parsed = urlparse(url) if parsed.scheme != "https": raise ValueError(f"Only HTTPS downloads are permitted (got: {parsed.scheme}://)") if parsed.netloc not in _ALLOWED_DOWNLOAD_DOMAINS: raise ValueError( f"Download from '{parsed.netloc}' is not allowed. " f"Permitted domains: {sorted(_ALLOWED_DOWNLOAD_DOMAINS)}" ) r = requests.get(url, stream=True, timeout=timeout) # noqa: AGENT-026 # nosemgrep: requests-no-timeout r.raise_for_status() with open(dest_path, "wb") as f: for chunk in r.iter_content(64 * 1024): if chunk: f.write(chunk) def install_gitleaks() -> str: if have_binary("gitleaks"): return "already installed" try: BIN_DIR.mkdir(parents=True, exist_ok=True) if _IS_WINDOWS: url = ( # noqa: AGENT-004 f"https://github.com/gitleaks/gitleaks/releases/download/" f"v{GITLEAKS_VERSION}/gitleaks_{GITLEAKS_VERSION}_windows_x64.zip" ) zip_path = BIN_DIR / "_gitleaks.zip" _stream_download(url, zip_path) with zipfile.ZipFile(zip_path) as z: for name in z.namelist(): if name.lower() in ("gitleaks.exe", "gitleaks"): z.extract(name, BIN_DIR) extracted = BIN_DIR / name target = BIN_DIR / "gitleaks.exe" if extracted != target: extracted.rename(target) break zip_path.unlink(missing_ok=True) else: url = ( # noqa: AGENT-004 f"https://github.com/gitleaks/gitleaks/releases/download/" f"v{GITLEAKS_VERSION}/gitleaks_{GITLEAKS_VERSION}_linux_x64.tar.gz" ) tar_path = BIN_DIR / "_gitleaks.tar.gz" _stream_download(url, tar_path) with tarfile.open(tar_path) as tar: for member in tar.getmembers(): if member.name == "gitleaks": tar.extract(member, BIN_DIR) break (BIN_DIR / "gitleaks").chmod(0o755) tar_path.unlink(missing_ok=True) return "installed" except Exception as e: return f"install failed: {e}" def install_hadolint() -> str: if have_binary("hadolint"): return "already installed" try: BIN_DIR.mkdir(parents=True, exist_ok=True) if _IS_WINDOWS: url = ( # noqa: AGENT-004 f"https://github.com/hadolint/hadolint/releases/download/" f"v{HADOLINT_VERSION}/hadolint-Windows-x86_64.exe" ) bin_path = BIN_DIR / "hadolint.exe" else: url = ( # noqa: AGENT-004 f"https://github.com/hadolint/hadolint/releases/download/" f"v{HADOLINT_VERSION}/hadolint-Linux-x86_64" ) bin_path = BIN_DIR / "hadolint" _stream_download(url, bin_path) if not _IS_WINDOWS: bin_path.chmod(0o755) return "installed" except Exception as e: return f"install failed: {e}" def bootstrap_binaries() -> dict: """Install missing binaries and ensure BIN_DIR is on PATH. Idempotent.""" BIN_DIR.mkdir(parents=True, exist_ok=True) bin_dir_str = str(BIN_DIR) path = os.environ.get("PATH", "") if bin_dir_str not in path.split(os.pathsep): os.environ["PATH"] = bin_dir_str + os.pathsep + path return { "gitleaks": install_gitleaks(), "hadolint": install_hadolint(), }