autoscan / core /bootstrap.py
Chris4K's picture
Initial commit v5.0.0.
5248e3b verified
"""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(),
}