#!/usr/bin/python3 import os import sys import subprocess import tempfile import shutil USER_VERSION = "8.27.0" FORCE_INSTALL = False SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__)) def parse_args(): global USER_VERSION, FORCE_INSTALL args = sys.argv[1:] new_args = [] while args: arg = args.pop(0) if arg.startswith("--version="): USER_VERSION = arg.split("=", 1)[1].lstrip("v") elif arg == "--force": FORCE_INSTALL = True else: new_args.append(arg) return new_args def is_gitleaks_installed(): try: result = subprocess.run(["gitleaks", "version"], capture_output=True, text=True) return result.returncode == 0 except FileNotFoundError: return False def get_installed_version(): try: result = subprocess.run(["gitleaks", "version"], capture_output=True, text=True) if result.returncode != 0: return None version = result.stdout.strip() if version.startswith("v"): version = version[1:] return version except Exception: return None def get_platform_and_arch(): import platform system = platform.system().lower() machine = platform.machine().lower() if system.startswith("linux"): plat = "linux" elif system == "darwin": plat = "darwin" elif system == "windows": plat = "windows" elif "mingw" in os.environ.get("MSYSTEM", "").lower(): plat = "windows" else: print(f"[!] Unsupported operating system: {system}") sys.exit(1) if machine in ["x86_64", "amd64"]: arch = "x64" elif machine in ["arm64", "aarch64"]: arch = "arm64" elif machine.startswith("armv7"): arch = "armv7" elif machine == "i386": arch = "x32" elif machine == "armv6": arch = "armv6" else: print(f"[!] Unsupported architecture: {machine}") sys.exit(1) if plat == "windows" and arch not in ["x32", "x64", "armv6", "armv7"]: print(f"[!] Unsupported Windows architecture: {arch}") sys.exit(1) return plat, arch def install_gitleaks(version, force): installed = is_gitleaks_installed() current_version = get_installed_version() if installed and not force: if current_version == version: return else: print(f"Gitleaks {current_version} is installed, but version {version} is required.") print(" Use --force to reinstall.") return print(f"Installing Gitleaks v{version}...") plat, arch = get_platform_and_arch() ext = "zip" if plat == "windows" else "tar.gz" filename = f"gitleaks_{version}_{plat}_{arch}.{ext}" url = f"https://github.com/gitleaks/gitleaks/releases/download/v{version}/{filename}" with tempfile.TemporaryDirectory() as tmpdir: archive_path = os.path.join(tmpdir, filename) print(f"Downloading from: {url}") subprocess.run(["curl", "-sSL", "-o", archive_path, url], check=True) if ext == "zip": import zipfile with zipfile.ZipFile(archive_path, "r") as zip_ref: zip_ref.extractall(tmpdir) else: subprocess.run(["tar", "-xzf", archive_path, "-C", tmpdir], check=True) exe_name = "gitleaks.exe" if plat == "windows" else "gitleaks" gitleaks_bin = os.path.join(tmpdir, exe_name) if plat == "windows": fallback = os.path.expanduser("~/.local/bin") os.makedirs(fallback, exist_ok=True) dest_path = os.path.join(fallback, exe_name) shutil.move(gitleaks_bin, dest_path) print(f"Gitleaks v{version} installed at {dest_path}") print("Add ~/.local/bin to your PATH if not already present.") else: install_path = "/usr/local/bin/gitleaks" elevate_cmd = None if shutil.which("sudo"): elevate_cmd = "sudo" elif shutil.which("doas"): elevate_cmd = "doas" if elevate_cmd: print(f"Installing with {elevate_cmd} at {install_path}...") try: subprocess.run([elevate_cmd, "mv", gitleaks_bin, install_path], check=True) subprocess.run([elevate_cmd, "chmod", "+x", install_path], check=True) print(f"Gitleaks v{version} installed at {install_path}") except subprocess.CalledProcessError: print(f"Error moving file with {elevate_cmd}. Installing locally...") fallback = os.path.expanduser("~/.local/bin") os.makedirs(fallback, exist_ok=True) shutil.move(gitleaks_bin, os.path.join(fallback, "gitleaks")) print(f"Gitleaks v{version} installed at {fallback}") print("Ensure ~/.local/bin is in your PATH") else: print("Neither 'sudo' nor 'doas' available. Installing locally...") fallback = os.path.expanduser("~/.local/bin") os.makedirs(fallback, exist_ok=True) shutil.move(gitleaks_bin, os.path.join(fallback, "gitleaks")) print(f"Gitleaks v{version} installed at {fallback}") print("Ensure ~/.local/bin is in your PATH") def main(): args = parse_args() install_gitleaks(USER_VERSION, FORCE_INSTALL) # hook variables TEMPLATE_NAME = "leet" CONFIG_FILE_NAME = "gitleaks.toml" FORMAT = "html" REDACT = False config_path = None use_internal_rules = False use_external_rules = False for arg in args: if arg.startswith("--template="): TEMPLATE_NAME = arg.split("=",1)[1] elif arg.startswith("--config="): config_path = arg.split("=",1)[1] elif arg.startswith("--format="): FORMAT = arg.split("=",1)[1] elif arg == "--redact": REDACT = True elif arg == "--rules-internal": use_internal_rules = True elif arg == "--rules-external": use_external_rules = True # By default, don't use any config file unless explicitly specified if not (use_internal_rules or use_external_rules): config_path = None elif not config_path: if use_internal_rules: config_path = os.path.join(SCRIPT_DIR, "rules", CONFIG_FILE_NAME) elif use_external_rules: # You might want to specify a default external rules path here print("Error: --rules-external requires a config file path with --config=") sys.exit(1) template_path = os.path.join(SCRIPT_DIR, "templates", f"{TEMPLATE_NAME}.tmpl") report_dir = os.path.join(".pre-commit", "gitleaks", "report") os.makedirs(report_dir, exist_ok=True) if FORMAT not in ("html", "json"): print(f"Unsupported format: {FORMAT}") print(" Supported formats: html, json") sys.exit(1) if config_path and not os.path.isfile(config_path): print(f"Config file not found: {config_path}") sys.exit(1) extra_args = [] if config_path: extra_args.append(f"--config={config_path}") if REDACT: extra_args.append("--redact") if FORMAT == "html": if not os.path.isfile(template_path): print(f"Template not found: {template_path}") sys.exit(1) report_path = os.path.join(report_dir, "index.html") cmd = ["gitleaks", "git"] + extra_args + [ "--report-format=template", f"--report-template={template_path}", f"--report-path={report_path}" ] else: report_path = os.path.join(report_dir, "report.json") cmd = ["gitleaks", "git"] + extra_args + [ "--report-format=json", f"--report-path={report_path}" ] print(f"Running: {' '.join(cmd)}") result = subprocess.run(cmd) if result.returncode != 0: print("Error running gitleaks.") sys.exit(result.returncode) if os.path.isfile(report_path): print(f"Report generated: {report_path}") else: print("Failed to generate report.") sys.exit(1) if __name__ == "__main__": main()