| """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"
|
|
|
|
|
| _ALLOWED_DOWNLOAD_DOMAINS = frozenset({"github.com", "objects.githubusercontent.com"})
|
|
|
|
|
|
|
|
|
| if _IS_WINDOWS:
|
| BIN_DIR = Path(sys.executable).parent
|
| else:
|
| 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)
|
| 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 = (
|
| 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 = (
|
| 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 = (
|
| 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 = (
|
| 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(),
|
| }
|
|
|