"""Tiered installer for security sidecar tools. Installation tiers: pip — Python packages (pycryptodome, pwntools, factordb-python, etc.) binary — Pre-built GitHub release binaries (nuclei, ffuf, dalfox, etc.) gem — Ruby gems (zap-cli, etc.) detect — Manual-detect-only (z3, sage, etc. — user must install themselves) """ import json import os import platform import shutil import subprocess import sys import tarfile import tempfile import urllib.request from dataclasses import dataclass, field from pathlib import Path from typing import Optional @dataclass class InstallResult: ok: bool = False message: str = "" installed: list[str] = field(default_factory=list) failed: list[str] = field(default_factory=list) skipped: list[str] = field(default_factory=list) class TieredInstaller: """Multi-tier installer for security tools. Detects the current platform and tries each tier in order of preference: pip -> binary -> gem -> detect (manual only). """ def __init__(self, target_dir: Optional[str] = None): self.target_dir = Path(target_dir or self._default_bin_dir()) self.target_dir.mkdir(parents=True, exist_ok=True) self._pip_available = shutil.which("pip3") is not None or shutil.which("pip") is not None self._gem_available = shutil.which("gem") is not None self._arch = platform.machine() self._os = platform.system().lower() def _default_bin_dir(self) -> str: if self._os == "linux": return "/usr/local/bin" return os.path.expanduser("~/.local/bin") # ── pip tier ──────────────────────────────────────────────────── def _pip_install(self, pkg: str) -> bool: pip = shutil.which("pip3") or shutil.which("pip") if not pip: return False try: result = subprocess.run( [pip, "install", "--quiet", pkg], capture_output=True, text=True, timeout=120, ) return result.returncode == 0 except subprocess.TimeoutExpired: return False except Exception: return False # ── GitHub release binary tier ────────────────────────────────── def _download_gh_release( self, repo: str, asset_pattern: str, ) -> Optional[Path]: api_url = f"https://api.github.com/repos/{repo}/releases/latest" try: req = urllib.request.Request(api_url, headers={ "Accept": "application/json", "User-Agent": "zesdex-security-daemon/0.1.0", }) with urllib.request.urlopen(req, timeout=30) as resp: data = json.loads(resp.read().decode()) assets = data.get("assets", []) os_arch_tag = f"{self._os}_{self._arch}" for asset in assets: name = asset["name"] if asset_pattern.replace("{os_arch}", os_arch_tag) in name: download_url = asset["browser_download_url"] break else: # Try without os_arch matching, just look for the pattern. candidates = [a for a in assets if asset_pattern.split("/")[0] in a["name"]] if not candidates: return None download_url = candidates[0]["browser_download_url"] with tempfile.NamedTemporaryFile(suffix=".tmp", delete=False) as tmp: with urllib.request.urlopen(download_url, timeout=120) as dl: tmp.write(dl.read()) tmppath = tmp.name if download_url.endswith(".tar.gz"): extract_dir = tempfile.mkdtemp() with tarfile.open(tmppath, "r:gz") as tar: tar.extractall(path=extract_dir) os.unlink(tmppath) # Find the binary in extracted files. for root, _dirs, files in os.walk(extract_dir): for fname in files: if fname == asset_pattern.split("/")[0].replace(".tar.gz", ""): return Path(root) / fname return None else: result = Path(tmppath) result.chmod(0o755) return result except Exception: return None def _install_binary(self, name: str, repo: str, asset_pattern: str) -> bool: downloaded = self._download_gh_release(repo, asset_pattern) if downloaded is None: return False dest = self.target_dir / name try: shutil.move(str(downloaded), str(dest)) dest.chmod(0o755) return True except Exception: return False # ── Gem tier ──────────────────────────────────────────────────── def _gem_install(self, gem_name: str) -> bool: if not self._gem_available: return False try: result = subprocess.run( ["gem", "install", "--quiet", gem_name], capture_output=True, text=True, timeout=120, ) return result.returncode == 0 except Exception: return False # ── Tool-specific installers ──────────────────────────────────── def _install_pip_tools(self) -> list[str]: pkgs = [ "pycryptodome", "pwntools", "requests", "factordb-python", ] installed = [] for pkg in pkgs: if self._pip_install(pkg): installed.append(pkg) return installed def _install_binary_tools(self) -> list[str]: targets = [ ("nuclei", "projectdiscovery/nuclei", "nuclei_{os_arch}.tar.gz"), ("ffuf", "ffuf/ffuf", "ffuf_{os_arch}.tar.gz"), ("dalfox", "hahwul/dalfox", "dalfox_{os_arch}.tar.gz"), ("httpx", "projectdiscovery/httpx", "httpx_{os_arch}.tar.gz"), ] installed = [] for name, repo, pattern in targets: if shutil.which(name) is not None: installed.append(name) continue if self._install_binary(name, repo, pattern): installed.append(name) return installed def _install_gem_tools(self) -> list[str]: installed = [] if self._gem_install("zap-cli"): installed.append("zap-cli") return installed # ── Public API ────────────────────────────────────────────────── def install_all(self) -> InstallResult: result = InstallResult() try: pip_ok = self._install_pip_tools() result.installed.extend(pip_ok) except Exception as e: result.failed.append(f"pip: {e}") try: bin_ok = self._install_binary_tools() result.installed.extend(bin_ok) except Exception as e: result.failed.append(f"binary: {e}") try: gem_ok = self._install_gem_tools() result.installed.extend(gem_ok) except Exception as e: result.failed.append(f"gem: {e}") result.ok = True result.message = ( f"Installed {len(result.installed)} tool(s): " f"{', '.join(result.installed)}" ) return result def install_tool(self, tool_name: str) -> dict: """Install a single tool by name. Returns {"ok": bool, "message": str}.""" pip_map = { "pycryptodome": "pycryptodome", "pwntools": "pwntools", "factordb": "factordb-python", "requests": "requests", } binary_map = { "nuclei": ("projectdiscovery/nuclei", "nuclei_{os_arch}.tar.gz"), "ffuf": ("ffuf/ffuf", "ffuf_{os_arch}.tar.gz"), "dalfox": ("hahwul/dalfox", "dalfox_{os_arch}.tar.gz"), "httpx": ("projectdiscovery/httpx", "httpx_{os_arch}.tar.gz"), "sqlmap": ("sqlmapproject/sqlmap", "sqlmap.tar.gz"), } gem_map = { "zap-cli": "zap-cli", } if tool_name in pip_map: ok = self._pip_install(pip_map[tool_name]) return {"ok": ok, "message": f"pip install {pip_map[tool_name]}: {'ok' if ok else 'failed'}"} if tool_name in binary_map: repo, pattern = binary_map[tool_name] ok = self._install_binary(tool_name, repo, pattern) return {"ok": ok, "message": f"binary install {tool_name}: {'ok' if ok else 'failed'}"} if tool_name in gem_map: ok = self._gem_install(gem_map[tool_name]) return {"ok": ok, "message": f"gem install {gem_map[tool_name]}: {'ok' if ok else 'failed'}"} return {"ok": False, "message": f"no installer available for {tool_name}; may need manual install"}