diff --git a/config.py b/config.py new file mode 100644 index 0000000..abc85ad --- /dev/null +++ b/config.py @@ -0,0 +1,43 @@ +import json +import logging +from pathlib import Path +from typing import Any + +from constants import USER_CONFIG_FILE, USER_TOOLS_DIR, DEFAULT_CONFIG + +logger = logging.getLogger(__name__) + + +def load() -> dict[str, Any]: + """Load config from disk, merging with defaults for any missing keys.""" + if USER_CONFIG_FILE.exists(): + try: + on_disk = json.loads(USER_CONFIG_FILE.read_text()) + return {**DEFAULT_CONFIG, **on_disk} + except (json.JSONDecodeError, OSError) as exc: + logger.warning("Config file unreadable (%s), using defaults.", exc) + return dict(DEFAULT_CONFIG) + + +def save(cfg: dict[str, Any]) -> None: + """Write config to disk, creating parent directories if needed.""" + USER_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True) + USER_CONFIG_FILE.write_text(json.dumps(cfg, indent=2, sort_keys=True)) + + +def get_tools_dir() -> Path: + """ + Return the directory where external tools are stored. + Creates it if it does not exist. + Always an absolute path — never relies on process CWD. + """ + cfg = load() + tools_dir = Path(cfg.get("tools_dir", str(USER_TOOLS_DIR))).expanduser().resolve() + tools_dir.mkdir(parents=True, exist_ok=True) + return tools_dir + + +def get_sudo_cmd() -> str: + """Return 'doas' if available, else 'sudo'. Never hardcode 'sudo'.""" + import shutil + return "doas" if shutil.which("doas") else "sudo" \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..b52b400 --- /dev/null +++ b/constants.py @@ -0,0 +1,65 @@ +from pathlib import Path +import platform +import shutil as _shutil + +# ── Repository ──────────────────────────────────────────────────────────────── +REPO_OWNER = "Z4nzu" +REPO_NAME = "hackingtool" +REPO_URL = f"https://github.com/{REPO_OWNER}/{REPO_NAME}.git" +REPO_WEB_URL = f"https://github.com/{REPO_OWNER}/{REPO_NAME}" + +# ── Versioning ──────────────────────────────────────────────────────────────── +VERSION = "2.0.0" +VERSION_DISPLAY = f"v{VERSION}" + +# ── Python requirement ──────────────────────────────────────────────────────── +MIN_PYTHON = (3, 10) + +# ── User-scoped paths (cross-platform, always computed at runtime) ───────────── +# NEVER hardcode /home/username — use Path.home() so it works for any user, +# including root (/root), regular users (/home/alice), macOS (/Users/alice). +USER_CONFIG_DIR = Path.home() / f".{REPO_NAME}" +USER_TOOLS_DIR = USER_CONFIG_DIR / "tools" +USER_CONFIG_FILE = USER_CONFIG_DIR / "config.json" +USER_LOG_FILE = USER_CONFIG_DIR / f"{REPO_NAME}.log" + +# ── System install paths (set per OS) ───────────────────────────────────────── +_system = platform.system() + +if _system == "Darwin": + # macOS — Homebrew convention + APP_INSTALL_DIR = Path("/usr/local/share") / REPO_NAME + APP_BIN_PATH = Path("/usr/local/bin") / REPO_NAME +elif _system == "Linux": + APP_INSTALL_DIR = Path("/usr/share") / REPO_NAME + APP_BIN_PATH = Path("/usr/bin") / REPO_NAME +else: + # Fallback (Windows, FreeBSD, etc.) + APP_INSTALL_DIR = USER_CONFIG_DIR / "app" + APP_BIN_PATH = USER_CONFIG_DIR / "bin" / REPO_NAME + +# ── UI theme ────────────────────────────────────────────────────────────────── +THEME_PRIMARY = "bold magenta" +THEME_BORDER = "bright_magenta" +THEME_SUCCESS = "bold green" +THEME_ERROR = "bold red" +THEME_WARNING = "bold yellow" +THEME_DIM = "dim white" +THEME_ARCHIVED = "dim yellow" +THEME_URL = "underline bright_blue" +THEME_ACCENT = "bold cyan" + +# ── Default config values ────────────────────────────────────────────────────── +DEFAULT_CONFIG: dict = { + "tools_dir": str(USER_TOOLS_DIR), + "version": VERSION, + "theme": "magenta", + "show_archived": False, + "sudo_binary": "sudo", + "go_bin_dir": str(Path.home() / "go" / "bin"), + "gem_bin_dir": str(Path.home() / ".gem" / "ruby"), +} + +# ── Privilege escalation ─────────────────────────────────────────────────────── +# Prefer doas if present (OpenBSD/some Linux setups), else sudo +PRIV_CMD = "doas" if _shutil.which("doas") else "sudo" \ No newline at end of file diff --git a/os_detect.py b/os_detect.py new file mode 100644 index 0000000..43ef87f --- /dev/null +++ b/os_detect.py @@ -0,0 +1,128 @@ +import platform +import shutil +from dataclasses import dataclass, field +from pathlib import Path + + +@dataclass +class OSInfo: + system: str # "linux", "macos", "windows", "unknown" + distro_id: str = "" # "kali", "ubuntu", "arch", "fedora", etc. + distro_like: str = "" # "debian", "rhel", etc. (from ID_LIKE) + distro_version: str = "" # "2024.1", "22.04", etc. + pkg_manager: str = "" # "apt-get", "pacman", "dnf", "brew", etc. + is_root: bool = False + home_dir: Path = field(default_factory=Path.home) + is_wsl: bool = False # Windows Subsystem for Linux + arch: str = "" # "x86_64", "aarch64", "arm64" + + +def detect() -> OSInfo: + """ + Fully detect the current OS, distro, and available package manager. + Never asks the user — entirely automatic. + """ + import os + + system_raw = platform.system() + system = system_raw.lower() + if system == "darwin": + system = "macos" + + info = OSInfo( + system = system, + is_root = (os.geteuid() == 0) if hasattr(os, "geteuid") else False, + home_dir = Path.home(), + arch = platform.machine(), + ) + + # ── Linux-specific ───────────────────────────────────────────────────────── + if system == "linux": + # Detect WSL + try: + info.is_wsl = "microsoft" in Path("/proc/version").read_text().lower() + except (FileNotFoundError, PermissionError): + pass + + # Read /etc/os-release (standard on all modern distros) + os_release: dict[str, str] = {} + for path in ("/etc/os-release", "/usr/lib/os-release"): + try: + for line in Path(path).read_text().splitlines(): + k, _, v = line.partition("=") + os_release[k.strip()] = v.strip().strip('"') + break + except FileNotFoundError: + continue + + info.distro_id = os_release.get("ID", "").lower() + info.distro_like = os_release.get("ID_LIKE", "").lower() + info.distro_version = os_release.get("VERSION_ID", "") + + # ── Package manager detection (in priority order) ────────────────────────── + for mgr in ("apt-get", "pacman", "dnf", "zypper", "apk", "brew", "pkg"): + if shutil.which(mgr): + info.pkg_manager = mgr + break + + return info + + +# Module-level singleton — computed once on import +CURRENT_OS: OSInfo = detect() + + +# ── Per-OS package manager commands ──────────────────────────────────────────── +PACKAGE_INSTALL_CMDS: dict[str, str] = { + "apt-get": "apt-get install -y {packages}", + "pacman": "pacman -S --noconfirm {packages}", + "dnf": "dnf install -y {packages}", + "zypper": "zypper install -y {packages}", + "apk": "apk add {packages}", + "brew": "brew install {packages}", +} + +PACKAGE_UPDATE_CMDS: dict[str, str] = { + "apt-get": "apt-get update -qq && apt-get upgrade -y", + "pacman": "pacman -Syu --noconfirm", + "dnf": "dnf upgrade -y", + "zypper": "zypper update -y", + "apk": "apk update && apk upgrade", + "brew": "brew update && brew upgrade", +} + +# Core system packages needed per package manager +REQUIRED_PACKAGES: dict[str, list[str]] = { + "apt-get": ["git", "python3-pip", "python3-venv", "curl", "wget", + "ruby", "ruby-dev", "golang-go", "php", "default-jre-headless"], + "pacman": ["git", "python-pip", "curl", "wget", + "ruby", "go", "php", "jre-openjdk-headless"], + "dnf": ["git", "python3-pip", "curl", "wget", + "ruby", "golang", "php", "java-17-openjdk-headless"], + "zypper": ["git", "python3-pip", "curl", "wget", "ruby", "go", "php"], + "brew": ["git", "python3", "curl", "wget", "ruby", "go", "php"], +} + + +def install_packages(packages: list[str], os_info: OSInfo | None = None) -> bool: + """Install system packages using the detected package manager.""" + import subprocess + if os_info is None: + os_info = CURRENT_OS + + mgr = os_info.pkg_manager + if mgr not in PACKAGE_INSTALL_CMDS: + print(f"[warning] Unknown package manager. Install manually: {packages}") + return False + + cmd_template = PACKAGE_INSTALL_CMDS[mgr] + pkg_str = " ".join(packages) + cmd = cmd_template.format(packages=pkg_str) + + # Prepend privilege escalation only on Linux (brew on macOS doesn't need sudo) + if os_info.system == "linux" and not os_info.is_root: + from constants import PRIV_CMD + cmd = f"{PRIV_CMD} {cmd}" + + result = subprocess.run(cmd, shell=True, check=False) + return result.returncode == 0 \ No newline at end of file diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/others/__init__.py b/tools/others/__init__.py new file mode 100644 index 0000000..e69de29