mirror of
https://github.com/Z4nzu/hackingtool
synced 2026-05-22 08:28:57 +00:00
* Phase 0: Add foundation files for v2.0.0 restructure
- constants.py: single source of truth for repo URLs, version (2.0.0),
all paths via Path.home(), UI theme constants, PRIV_CMD auto-detection
- os_detect.py: OSInfo dataclass, auto-detect OS/distro/package manager,
CURRENT_OS singleton, per-OS install command maps
- config.py: get_tools_dir(), load()/save() config.json, get_sudo_cmd()
- tools/__init__.py, tools/others/__init__.py: make proper Python packages
- IMPLEMENTATION.md: full 18-section restructuring plan (2350+ lines)
- LOG.md: 13-phase progress tracker
* Phase 1+2: Fix all 30 critical bugs and 4 security vulnerabilities
* Phase 3: Installation & path overhaul
* Phase 4+5: Core architecture + shared console across all tool files
Phase 4 (core.py — largely done in Phase 1, completed here):
- HackingTool: add ARCHIVED, ARCHIVED_REASON, SUPPORTED_OS, REQUIRES_* fields
- HackingTool: remove INSTALLATION_DIR (unused)
- HackingToolsCollection: add _active_tools(), _archived_tools(), _incompatible_tools()
- HackingToolsCollection: add _show_archived_tools() (option 98 sub-menu)
- HackingToolsCollection.show_options(): filter by OS and ARCHIVED flag
- OS-incompatible tools show count but are hidden from menu
- Archived tools accessible via option 98 with reason displayed
Phase 5 (all 22 remaining tool files):
- Remove local console = Console() and _theme = Theme() from all 22 files
- Remove P_COLOR and PURPLE_STYLE local constants
- Add `from core import HackingTool, HackingToolsCollection, console` everywhere
- Remove show_options() overrides from all collection classes (500+ lines deleted)
- Remove pretty_print() overrides from all collection classes
- Remove _get_attr() / _get_attr_fallback() helpers from all collection classes
- Replace super(ClassName, self).__init__() → super().__init__() in all files
- Remove # coding=utf-8 headers from all files
- Fix remaining PURPLE_STYLE usages → "bold magenta" literal
All 28 tool modules import cleanly. Zero local console instances remain.
* Phase 6: Naming & structure cleanup
- Rename tools/phising_attack.py → phishing_attack.py (fix typo)
- Rename tools/information_gathering_tools.py → information_gathering.py
- Rename tools/wireless_attack_tools.py → wireless_attack.py
- Rename tools/forensic_tools.py → forensics.py
- Rename tools/sql_tools.py → sql_injection.py
- Rename tools/webattack.py → web_attack.py
- Rename class autophisher → Autophisher (PEP-8 CapWords)
- Rename class ddos → DDoSTool (PEP-8 CapWords), TITLE "ddos" → "DDoS"
- Update all imports in hackingtool.py and exploit_frameworks.py
* Phase 7: Tool install command fixes
- Replace setup.py install → pip install --user . (explo, takeover, reconspider, infoga)
- Mark Blazy as ARCHIVED: Python 2 only (pip2.7/python2.7)
- Replace sudo git clone → git clone everywhere (no root needed for user tools dir)
- Replace sudo pip install → pip install --user everywhere
- Fix dalfox: git clone + cd approach → go install github.com/hahwul/dalfox/v2@latest
- Add apt -y flag to ReconSpider apt install
* Phase 8: Mark archived tools and add SUPPORTED_OS flags
- Remove stale sys.path.append hack from forensics.py (unused since package structure)
- Add SUPPORTED_OS = ["linux"] to BulkExtractor, Guymager (apt/Linux-only)
- Add SUPPORTED_OS = ["linux"] to all android_attack.py tools (bash scripts)
- Add SUPPORTED_OS = ["linux"], REQUIRES_WIFI = True to wifi_jamming.py tools
- Fix WifiJammerNG RUN_COMMANDS: python → python3, remove boxes/lolcat pipe
* Phase 11: requirements.txt and Dockerfile cleanup
- requirements.txt: remove boxes, lolcat (system CLI tools not pip packages),
flask (unused), requests (unused at runtime); pin rich>=13.0.0
- Dockerfile: add --break-system-packages to pip3 install (PEP 668 compliance
on Kali/Debian externally-managed Python environments)
* Phase 13: Python 3 modernization and os.system cleanup
- Replace os.system("cd X; ...") no-op cd subshells with subprocess.run(cwd=...)
in: xss_attack.py (XSSCon, XanXSS), payload_creator.py (TheFatRat update/troubleshoot),
forensics.py (BulkExtractor gui/cli), phishing_attack.py (BlackPhish update)
- Replace os.system echo+boxes+lolcat in post_exploitation.py with console.print
- Fix socialmedia_finder.py: print()+os.system+lolcat → subprocess+console.print
- Fix forensics.py cli_mode: os.system apt/bulk_extractor → subprocess.run list form
* Phase 13 (cont): Final os.system cleanup
- anonsurf.py: os.system("sudo anonsurf stop") → subprocess.run list form
- tool_manager.py: os.system(f"{priv}{cmd}") → subprocess.run(shell=True)
(shell=True justified: cmd is from hardcoded PACKAGE_UPDATE_CMDS dict, not user input)
* Phase 9: Add SUPPORTED_OS flags to remaining Linux-only tools
Mark bash-script and apt-dependent tools as linux-only so they are
hidden automatically on macOS via _active_tools() filter in core.py:
- phishing_attack.py: 14 tools (all bash-script based)
- ddos.py: all 6 tools (Linux network stack required)
- post_exploitation.py: Vegile, ChromeKeyLogger
- payload_creator.py: all 7 tools (bash setup scripts)
- exploit_frameworks.py: WebSploit (bash install)
- reverse_engineering.py: Apk2Gold (bash build)
- xss_attack.py: RVuln (Rust + apt deps)
* Phase 10: Add modern tools across 6 categories
Information Gathering (+7):
theHarvester, Amass, Masscan, RustScan, Holehe, Maigret, httpx
Web Attack (+6):
Nuclei, ffuf, Feroxbuster, Nikto, wafw00f, Katana
Wordlist/Password (+3):
Hashcat, John the Ripper, haiti
Wireless Attack (+3):
Airgeddon, hcxdumptool, hcxtools
Forensics (+2):
Volatility3, Binwalk
Post Exploitation (+1):
pwncat-cs
* Phase 12: Update README for v2.0.0
- Version badge: v1.2.0 -> v2.0.0
- Python badge: Python-3 -> Python-3.10+
- Add macOS to platform badge
- Add 'What's new in v2.0.0' section replacing stale v1.2.0 notes
- Regenerate full tool list — 22 new tools marked with new
- Fix docker build typo: 'docker buitl' -> 'docker build'
- Add Requirements section (Python 3.10+, Go 1.21+, Ruby)
- Remove stale 'To do' checklist and old Docker output block
* Redesign README, templates, and update .github workflows
README.md:
- Modern centered header with full badge row
- Stats banner: 17 categories, 150+ tools
- What's new as a comparison table
- Tool category table with per-category counts
- Complete tool listing with star markers for new tools
- Contributing section with enforced title formats for issues and PRs
- Star History chart (Z4nzu/hackingtool)
- Cleaned up Installation, Requirements, Social sections
README_template.md:
- Full redesign matching README.md layout
- {{toc}} and {{tools}} placeholders preserved for generate_readme.py
- Contributing section with title format rules embedded in template
.github/ISSUE_TEMPLATE/bug_report.md:
- Remove irrelevant browser/smartphone sections
- Add tool name, category, error output, environment table
.github/ISSUE_TEMPLATE/feature_request.md:
- Tighten to 4 focused fields, note tool additions use separate template
.github/ISSUE_TEMPLATE/tool_request.md (new):
- Structured template for new tool suggestions
- Required fields: name, URL, category, OS, install/run commands, reason
- Enforced title format: [Tool Request] ToolName - Category
.github/PULL_REQUEST_TEMPLATE.md (new):
- PR checklist for new tool additions and other change types
- Enforced title format: [New Tool] / [Fix] / [Improve]
- All required class fields listed as checklist items
.github/workflows/lint_python.yml:
- actions/checkout v3 -> v4, setup-python v4 -> v5
- python-version: 3.x -> 3.12 (project requires 3.10+)
- ruff target-version: py37 -> py310
- ruff --show-source -> --output-format=full (updated flag name)
- Remove deprecated safety check command
.github/workflows/test_install.yml:
- actions/checkout v3 -> v4, setup-python v4 -> v5
- python-version: 3.x -> 3.12
- Replace fragile matrix of hard-coded menu numbers with
two simple smoke tests: launch+quit, and navigate+back
* Improve Dockerfile, docker-compose, add .dockerignore
Dockerfile:
- Add '# syntax=docker/dockerfile:1' to enable BuildKit features
- Add LABEL metadata (OCI image spec)
- Remove unused apt packages: sudo, python3-venv
- Replace --no-cache-dir with --mount=type=cache for pip (faster rebuilds)
- Add comments explaining each decision
docker-compose.yml:
- Remove deprecated 'version:' field (Compose v2 ignores it, shows warning)
- Add 'image: hackingtool:latest' tag for clarity
- Add 'restart: unless-stopped' for production service
- Add 'hackingtool-dev' profile service with live source volume mount
so dev workflow (edit without rebuild) is separate from default run
- Clarify volume purpose in comments
.dockerignore (new):
- Exclude .git/, images/, __pycache__/, .github/, *.md, tests/
- Prevents multi-hundred-MB build context; dramatically reduces image size
- Keeps layer cache more stable (README changes no longer bust COPY layer)
README.md / README_template.md:
- Replace single-line Docker snippet with 3-option step-by-step guide:
Option A: docker run -it --rm (no Compose)
Option B: docker compose up -d + exec (recommended)
Option C: docker compose --profile dev (live source mount)
- Add docker compose down / down -v stop instructions
* Fix Docker sudo error, 99 back navigation, and empty input exit
install.py:
- sudo prefix now determined by euid (== 0 means root) not by OS name
- Previously used 'sudo ' for all Linux — breaks inside Docker where we
run as root but sudo is not installed
- Single priv variable computed once at top of install_system_packages()
Dockerfile:
- Add python3-venv back to apt installs
- Was removed as 'unused' but install.py uses 'python3 -m venv' to create
the virtualenv in APP_INSTALL_DIR; missing package caused venv failure
core.py (HackingTool.show_options + HackingToolsCollection.show_options):
- 99 always returns now instead of sys.exit() when parent is None
Previously: sub-menus with no parent called sys.exit() — since
interact_menu() never passes parent, pressing 99 in any category
exited the entire program instead of returning to the main menu
- Empty Enter (blank input) now continues the loop instead of
defaulting to "99" and triggering exit
Previously: Prompt default="99" meant pressing Enter without typing
anything was treated as choosing exit
- "Exit" label on 99 row changed to "Main Menu" since it now returns
* Restore Description column and 1-based numbering in collection menus
HackingToolsCollection.show_options():
- Add Description column back (was dropped during restructure)
- Show first line of description; '—' for tools with no description
- Switch to box.SIMPLE_HEAD with show_lines=True for row separators
- Numbering changed from 0-based to 1-based (index 1..N)
- Input validation updated: 1 <= choice <= len(active) with active[choice-1]
* Redesign main menu: compact header, 2-column grid, 1-based numbering, ? help
Header:
- Replace 8-line pixelated ASCII logo with a compact 9-line styled panel
- Shows tool name, version, repo URL, and legal warning in one block
- Screen is now cleared inside build_menu() on each main menu render
Menu layout:
- Single narrow column → 2-column grid (9 left / 8 right)
- Full terminal width is used; short display labels prevent truncation
- tool_definitions gains a third field (menu_label) for the grid;
full TITLE is still used when entering a category
Numbering:
- 0-based → 1-based throughout interact_menu()
- ToolManager is item 18 (was mislabeled 99, conflicting with exit)
- Input validation: 1 <= choice <= len(all_tools) → all_tools[choice - 1]
Commands:
- Add ? / help → show Quick Help overlay (main/category/tool reference)
- Add q / quit / exit → clean exit with goodbye message
- Remove Confirm.ask("Return to main menu?") — pressing 99 in any
sub-menu already returns here; the extra prompt was redundant friction
- Ctrl+C still works as force quit
Error messages updated to reflect new command set
* Redesign header: hacker split-layout with ASCII art + live system info
Replace compact text panel with a Metasploit-style split header:
- Left: "HT" block-letter ASCII art in bright green
- Vertical │ separator that runs the full height of the art block
- Right: live system stats — os, kernel, user@host, ip, tool count, session time
- Bottom: random hacker quote (10 options, changes on each launch)
- Bottom: authorized-use warning in dim red
- Panel title: '[ HackingTool v2.0.0 ]' left-aligned (terminal bracket style)
- Panel subtitle: '[ timestamp ]' right-aligned
- Border: bright_magenta HEAVY box
Live info gathered via platform, socket, os.getlogin() with fallbacks:
- OS: platform.freedesktop_os_release() → PRETTY_NAME, else platform.system()
- IP: UDP connect trick (no data sent, gets local outbound IP)
- User: os.getlogin() → USER env → LOGNAME env → 'root'
* Redesign header: full HACKING TOOL art + ? and q in all sub-menus
hackingtool.py:
- Replace 6-line "HT" art with full 12-line "HACKING TOOL" block letters
- Right side gains 3 more info lines: python version, arch, status
- 12 art lines paired with 12 stat lines for consistent separator
- Layout optimized for wide terminals (100+ chars); gracefully
truncates on narrow ones
core.py — ? and q support in all sub-menus:
- Add _show_inline_help() function: compact navigation reference
(1-N select, 99 back, 98 project page, ? help, q quit)
- HackingTool.show_options(): parse ? → inline help, q → SystemExit
- HackingToolsCollection.show_options(): same ? and q handling
- Both menus now show hint bar: "Enter number · ? help · q quit"
- Prompt changed from "[?] Select" to ">" for consistency with main menu
- q/quit/exit raises SystemExit(0) to cleanly exit from any depth
* Add install.sh one-liner and fix install.py to detect local source
install.sh (new):
- Standalone bash installer for curl | sudo bash one-liner
- Detects package manager (apt-get, pacman, dnf, brew)
- Installs prerequisites: git, python3, pip, venv
- Checks Python >= 3.10
- Clones repo with --depth 1 (shallow, faster)
- Creates venv + installs requirements
- Creates /usr/bin/hackingtool launcher
- Creates ~/.hackingtool/ dirs with correct ownership (SUDO_USER)
install.py:
- Add _is_source_dir() — detects if install.py is being run from a local
clone (hackingtool.py exists alongside it)
- install_source() replaces git_clone() — copies source to /usr/share/
instead of re-cloning when running from a local clone
- Falls back to git clone --depth 1 when not in a source directory
- Eliminates the redundant clone-after-clone pattern
README.md / README_template.md:
- Add one-liner install as primary method
- Keep manual git clone + install.py as alternative
- Remove sudo from hackingtool run command (launcher handles it)
* Add 35 new tools across 3 new + 6 existing categories
New categories:
- tools/active_directory.py: BloodHound, NetExec (nxc), Impacket,
Responder, Certipy, Kerbrute (6 tools)
- tools/cloud_security.py: Prowler, ScoutSuite, Pacu, Trivy (4 tools)
- tools/mobile_security.py: MobSF, Frida, Objection (3 tools)
Existing categories expanded:
- information_gathering.py: +SpiderFoot, Subfinder, TruffleHog, Gitleaks (4)
- web_attack.py: +Gobuster, Dirsearch, OWASP ZAP, testssl.sh, Arjun,
Caido, mitmproxy (7)
- post_exploitation.py: +Sliver, Havoc, PEASS-ng, Ligolo-ng, Chisel,
Evil-WinRM, Mythic (7)
- reverse_engineering.py: +Ghidra, Radare2 (2)
- forensics.py: +pspy (1)
- wireless_attack.py: +Bettercap (1)
hackingtool.py:
- Import 3 new category modules
- Add 3 new entries to tool_definitions (AD, Cloud, Mobile)
- Add 3 new instances to all_tools list
- Categories: 17 -> 20, total tools: 150+ -> 185+
- Help overlay updated for new range (1-20, 21=Update)
* Feature: Search tools by name or keyword (/ or s)
- Add _collect_all_tools() — walks all collections recursively and
returns (tool_instance, category_name) pairs for 185 tools
- Add search_tools() — prompts for query, matches against TITLE and
DESCRIPTION (case-insensitive), shows results table with category,
user selects a result number to jump directly into tool.show_options()
- Wire / and s commands into interact_menu()
- Add / search to help overlay and hint bar
- Fix ToolManager row number: was hardcoded 18, now computed dynamically
from len(categories) + 1 (currently 21)
* Feature: Show installed status (✔/✘) next to each tool
core.py:
- Add is_installed property to HackingTool class
Checks: (1) shutil.which() for binary from first RUN_COMMAND,
(2) os.path.isdir() for git clone target directory
Handles "cd foo && binary" and "sudo binary" patterns
- Add status column to HackingToolsCollection.show_options() table
✔ (green) = installed, ✘ (dim) = not installed
- Archived/back rows updated for extra column
* Feature: Tag-based filtering with auto-derived tags (t command)
core.py:
- Add TAGS field to HackingTool class (list[str], default empty)
- Allows manual tag override per tool
hackingtool.py:
- Add _get_all_tags() — builds tag index from 19 regex rules that
auto-derive tags from tool TITLE + DESCRIPTION (osint, scanner, c2,
web, cloud, mobile, wireless, forensics, reversing, etc.)
- Manual TAGS on a tool class take priority over auto-derived
- Add filter_by_tag() — shows all available tags with tool counts,
user picks a tag, results shown with installed status, select to
jump directly into tool.show_options()
- Wire t/tag/tags/filter commands into interact_menu()
- Search also matches against TAGS field
- Updated hint bar: / search · t tags · ? help · q quit
- Updated help overlay with tag filter entry
* Feature: Install all tools in a category (option 97)
HackingToolsCollection.show_options():
- Count not-installed tools using is_installed property
- Show option 97 'Install all (N not installed)' when N > 0
- On select: iterates all not-installed tools, calls tool.install()
with progress counter (1/N, 2/N, ...) and error handling per tool
- Hidden when all tools are already installed
- Updated inline help to mention option 97
* Feature: Update command for each tool (option 3 in tool menu)
HackingTool:
- Add update() method — detects install method from INSTALL_COMMANDS
and runs the appropriate update:
git clone → git -C <dir> pull
pip install → pip install --upgrade
go install → re-runs go install (fetches latest)
gem install → gem update
- Checks is_installed first; shows warning if not installed
- Added as option 3 in every tool's OPTIONS menu (Install, Run, Update)
* Feature: Tool recommendations — "I want to do X" (r command)
- Add _RECOMMENDATIONS dict — maps 22 common tasks to tag names:
"scan a network" → scanner, "crack passwords" → bruteforce/credentials,
"pentest active directory" → active-directory, "pivot through network"
→ network, etc.
- Add recommend_tools() — shows numbered task list, user picks one,
shows all matching tools with installed status, select to jump in
- Wire r/rec/recommend into interact_menu()
- Updated hint bar and help overlay
* Update README with all new features, 3 new categories, 35 new tools
- Stats banner: 17 → 20 categories, 150+ → 185+ tools
- What's New table: add 10 new feature entries (search, tags, recommend,
install status, install all, update, one-liner, new categories/tools)
- Add Quick Commands reference table (/, t, r, ?, q, 97, 99)
- Tool Categories table: add Active Directory, Cloud Security,
Mobile Security rows; update tool counts for expanded categories
- Add 3 new category sections: Active Directory Tools (6), Cloud
Security Tools (4), Mobile Security Tools (3) with GitHub links
- Mark all 35 new tools with ★ in their respective category sections
* UI: Dual-line prompt and inline /query search
Prompt style (all menus):
- Replace bare '>' with '╰─>' connected to the hint bar above
- Hint bar uses Claude CLI style: first letter highlighted in cyan,
rest dimmed (/search tags recommend ?help quit)
- Creates a visual dual-line input area similar to Claude Code CLI
Inline search:
- Typing /subdomain at the main prompt immediately searches and
shows results — no second "enter query" prompt needed
- / (bare slash) still opens the interactive search prompt
- s still works as a search alias
- search_tools() now accepts optional query parameter
core.py sub-menus:
- Same dual-line prompt style: hint bar + ╰─>
- Compact hint: ?help quit 99 back
* Fix is_installed crash on sub-collections (OtherTools)
OtherTools.TOOLS contains HackingToolsCollection instances (like
SocialMediaBruteforceTools) which don't have the is_installed property.
- Guard is_installed access with hasattr() in both the status column
and the not_installed count for Install All
- Sub-collections show blank status; individual tools show ✔/✘
* Add Buy Me a Coffee link to README and FUNDING.yml
- README.md: Add Support section with Buy Me a Coffee button above Social
- .github/FUNDING.yml: Add buy_me_a_coffee: hardikzinzu
(enables GitHub Sponsor button on the repo page)
* Feature: Open Folder option to manually access tool directory
HackingTool:
- Add _get_tool_dir() — finds the tool's local directory by checking:
1. Git clone target dir (from INSTALL_COMMANDS)
2. "cd foo && bar" pattern in RUN_COMMANDS
3. Binary location via shutil.which() → dirname
- Add open_folder() — opens the tool's directory in a new shell ($SHELL)
so the user can inspect files, run manual install steps, or debug
If dir not found: shows a helpful message with git clone command
- Added as option 4 in every tool menu (Install, Run, Update, Open Folder)
- Updated inline help to mention the tool menu options
* Add SVG logo and update README header
images/logo.svg:
- Terminal-style dark background with rounded corners
- Red/yellow/green window dots (macOS style)
- Green shield with skull & crossbones icon
- "HACKING" in purple-to-pink gradient with glow
- "TOOL" in hacker green with glow effect
- Blinking cursor animation, binary numbers, corner brackets
- Monospace font, 600x200 viewport
README.md:
- Replace plain # HackingTool heading with centered logo SVG
* Redesign README with modern GitHub features
Header:
- for-the-badge style badges with logos and custom colors
- Stats counter table (20 categories, 185+ tools, 19 tags, 2 platforms)
- CTA buttons: Install Now, Quick Commands, Suggest a Tool
What's New:
- Wrapped in collapsible <details open> with icon-prefixed table rows
- Each feature has an emoji icon for visual scanning
Quick Commands:
- Centered table with Works-in column showing scope
Tool Categories:
- 2-column table layout showing all 20 categories side by side
- Wrapped in collapsible section
Tool lists:
- Every category wrapped in collapsible <details> sections
- Keeps README scannable — users expand only what they need
- New tools still marked with star
Contributing:
- Side-by-side table: Issue template left, PR template right
Installation:
- Side-by-side table: One-liner left, Manual right
- Docker in collapsible section (not everyone needs it)
Requirements:
- Table format with version, dependency, and "needed for" columns
Support/Social:
- Centered layout with for-the-badge Twitter/GitHub follow buttons
- Larger Buy Me a Coffee button
* Redesign README header, add category emojis, left-align support
Header:
- Consolidate badges into 2 clean rows instead of 4 sparse rows
- Row 1: flat-square badges (license, python, version, stars, forks,
issues, last commit) — compact, inline, no awkward gaps
- Row 2: for-the-badge stat pills (20 Categories, 185+ Tools, 19 Tags,
Linux|Kali|Parrot|macOS) — single row, removes duplicate Platform badge
- CTA buttons tightened with consistent spacing
Category headings:
- Add emoji to every ## category heading throughout the README
(matching the index table emojis)
Support section:
- Left-aligned instead of centered
- Social split into its own heading
Also removes all <details>/<summary> collapse wrappers — all sections
are plain visible as requested
* Fix missing imports: subprocess in payload_creator, Table in wordlist_generator
- payload_creator.py: add 'import subprocess' (used by TheFatRat methods)
- wordlist_generator.py: add 'from rich.table import Table' (used by show_options)
- Fixes 6 ruff F821 'Undefined name' errors
* Fix codespell error: 'ags' flagged as misspelling in hint bar
Restructure the hint bar string so shortcut letters and their labels
are separate tokens ('t' + 'tags') instead of split across Rich markup
tags ('t' + 'ags') which codespell flagged as a misspelling.
* Fix 12 issues from Copilot PR review (#590)
post_exploitation.py:
- Rename INSTALL_OS -> SUPPORTED_OS in Havoc class (typo, field was ignored)
- Sliver: replace curl|sudo bash pipe with download-then-execute pattern
ddos.py:
- Add DDoSTool() to DDOSTools.TOOLS list (was defined but unreachable)
phishing_attack.py:
- Rename class Evilginx2 -> Evilginx3 (installs v3 via go install)
- Update instance in TOOLS list to match
- Fix stale comment: wireless_attack_tools.py -> wireless_attack.py
forensics.py:
- Remove installable=False from Guymager (conflicted with INSTALL_COMMANDS)
tool_manager.py:
- Skip sudo prefix when already root (os.geteuid() == 0), matching
the pattern already used in install.py
install.py:
- Add chown -R root:root after cp -a to prevent git "dubious ownership"
errors when the source clone has different ownership
update.sh:
- Add git config safe.directory before pull to prevent dubious ownership
- Add --upgrade flag to pip install so dependencies actually update
os_detect.py:
- Add pkg (FreeBSD) entries to PACKAGE_INSTALL_CMDS, PACKAGE_UPDATE_CMDS,
and REQUIRED_PACKAGES — was detected but had no command mappings (KeyError)
Skipped (not applicable):
- #1 subprocess import: already fixed in prior commit
- #11 Path.home() under sudo: by design (installer runs as root)
489 lines
19 KiB
Python
489 lines
19 KiB
Python
import os
|
||
import shutil
|
||
import sys
|
||
import webbrowser
|
||
from collections.abc import Callable
|
||
from platform import system
|
||
|
||
from rich import box
|
||
from rich.console import Console
|
||
from rich.panel import Panel
|
||
from rich.prompt import Prompt
|
||
from rich.table import Table
|
||
from rich.text import Text
|
||
from rich.theme import Theme
|
||
from rich.traceback import install
|
||
|
||
from constants import (
|
||
THEME_PRIMARY, THEME_BORDER, THEME_ACCENT,
|
||
THEME_SUCCESS, THEME_ERROR, THEME_WARNING,
|
||
THEME_DIM, THEME_ARCHIVED, THEME_URL,
|
||
)
|
||
|
||
# Enable rich tracebacks globally
|
||
install()
|
||
|
||
_theme = Theme({
|
||
"purple": "#7B61FF",
|
||
"success": THEME_SUCCESS,
|
||
"error": THEME_ERROR,
|
||
"warning": THEME_WARNING,
|
||
"archived": THEME_ARCHIVED,
|
||
"url": THEME_URL,
|
||
"dim": THEME_DIM,
|
||
})
|
||
|
||
# Single shared console — all tool files do: from core import console
|
||
console = Console(theme=_theme)
|
||
|
||
|
||
def clear_screen():
|
||
os.system("cls" if system() == "Windows" else "clear")
|
||
|
||
|
||
def validate_input(ip, val_range: list) -> int | None:
|
||
"""Return the integer if it is in val_range, else None."""
|
||
if not val_range:
|
||
return None
|
||
try:
|
||
ip = int(ip)
|
||
if ip in val_range:
|
||
return ip
|
||
except (TypeError, ValueError):
|
||
pass
|
||
return None
|
||
|
||
|
||
def _show_inline_help():
|
||
"""Quick help available from any menu level."""
|
||
console.print(Panel(
|
||
Text.assemble(
|
||
(" Navigation\n", "bold white"),
|
||
(" ─────────────────────────────────\n", "dim"),
|
||
(" 1–N ", "bold cyan"), ("select item\n", "white"),
|
||
(" 97 ", "bold cyan"), ("install all (in category)\n", "white"),
|
||
("\n Tool menu: Install, Run, Update, Open Folder\n", "dim"),
|
||
(" 99 ", "bold cyan"), ("go back\n", "white"),
|
||
(" 98 ", "bold cyan"), ("open project page / archived\n", "white"),
|
||
(" ? ", "bold cyan"), ("show this help\n", "white"),
|
||
(" q ", "bold cyan"), ("quit hackingtool\n", "white"),
|
||
),
|
||
title="[bold magenta] ? Quick Help [/bold magenta]",
|
||
border_style="magenta",
|
||
box=box.ROUNDED,
|
||
padding=(0, 2),
|
||
))
|
||
Prompt.ask("[dim]Press Enter to return[/dim]", default="")
|
||
|
||
|
||
class HackingTool:
|
||
TITLE: str = ""
|
||
DESCRIPTION: str = ""
|
||
INSTALL_COMMANDS: list[str] = []
|
||
UNINSTALL_COMMANDS: list[str] = []
|
||
RUN_COMMANDS: list[str] = []
|
||
OPTIONS: list[tuple[str, Callable]] = []
|
||
PROJECT_URL: str = ""
|
||
|
||
# OS / capability metadata
|
||
SUPPORTED_OS: list[str] = ["linux", "macos"]
|
||
REQUIRES_ROOT: bool = False
|
||
REQUIRES_WIFI: bool = False
|
||
REQUIRES_GO: bool = False
|
||
REQUIRES_RUBY: bool = False
|
||
REQUIRES_JAVA: bool = False
|
||
REQUIRES_DOCKER: bool = False
|
||
|
||
# Tags for search/filter (e.g. ["osint", "web", "recon", "scanner"])
|
||
TAGS: list[str] = []
|
||
|
||
# Archived tool flags
|
||
ARCHIVED: bool = False
|
||
ARCHIVED_REASON: str = ""
|
||
|
||
def __init__(self, options=None, installable=True, runnable=True):
|
||
options = options or []
|
||
if not isinstance(options, list):
|
||
raise TypeError("options must be a list of (option_name, option_fn) tuples")
|
||
self.OPTIONS = []
|
||
if installable:
|
||
self.OPTIONS.append(("Install", self.install))
|
||
if runnable:
|
||
self.OPTIONS.append(("Run", self.run))
|
||
self.OPTIONS.append(("Update", self.update))
|
||
self.OPTIONS.append(("Open Folder", self.open_folder))
|
||
self.OPTIONS.extend(options)
|
||
|
||
@property
|
||
def is_installed(self) -> bool:
|
||
"""Check if the tool's binary is on PATH or its clone dir exists."""
|
||
if self.RUN_COMMANDS:
|
||
cmd = self.RUN_COMMANDS[0]
|
||
# Handle "cd foo && binary --help" pattern
|
||
if "&&" in cmd:
|
||
cmd = cmd.split("&&")[-1].strip()
|
||
if cmd.startswith("sudo "):
|
||
cmd = cmd[5:].strip()
|
||
binary = cmd.split()[0] if cmd else ""
|
||
if binary and binary not in (".", "echo", "cd"):
|
||
if shutil.which(binary):
|
||
return True
|
||
# Check if git clone target dir exists
|
||
if self.INSTALL_COMMANDS:
|
||
for ic in self.INSTALL_COMMANDS:
|
||
if "git clone" in ic:
|
||
parts = ic.split()
|
||
repo_url = [p for p in parts if p.startswith("http")]
|
||
if repo_url:
|
||
dirname = repo_url[0].rstrip("/").rsplit("/", 1)[-1].replace(".git", "")
|
||
if os.path.isdir(dirname):
|
||
return True
|
||
return False
|
||
|
||
def show_info(self):
|
||
desc = f"[cyan]{self.DESCRIPTION}[/cyan]"
|
||
if self.PROJECT_URL:
|
||
desc += f"\n[url]🔗 {self.PROJECT_URL}[/url]"
|
||
if self.ARCHIVED:
|
||
desc += f"\n[archived]⚠ ARCHIVED: {self.ARCHIVED_REASON}[/archived]"
|
||
console.print(Panel(
|
||
desc,
|
||
title=f"[{THEME_PRIMARY}]{self.TITLE}[/{THEME_PRIMARY}]",
|
||
border_style="purple",
|
||
box=box.DOUBLE,
|
||
))
|
||
|
||
def show_options(self, parent=None):
|
||
"""Iterative menu loop — no recursion, no stack growth."""
|
||
while True:
|
||
clear_screen()
|
||
self.show_info()
|
||
|
||
table = Table(title="Options", box=box.SIMPLE_HEAVY)
|
||
table.add_column("No.", style="bold cyan", justify="center")
|
||
table.add_column("Action", style="bold yellow")
|
||
|
||
for index, option in enumerate(self.OPTIONS):
|
||
table.add_row(str(index + 1), option[0])
|
||
|
||
if self.PROJECT_URL:
|
||
table.add_row("98", "Open Project Page")
|
||
table.add_row("99", f"Back to {parent.TITLE if parent else 'Main Menu'}")
|
||
console.print(table)
|
||
console.print(
|
||
" [dim cyan]?[/dim cyan][dim]help "
|
||
"[/dim][dim cyan]q[/dim cyan][dim]uit "
|
||
"[/dim][dim cyan]99[/dim cyan][dim] back[/dim]"
|
||
)
|
||
|
||
raw = Prompt.ask("[bold cyan]╰─>[/bold cyan]", default="").strip().lower()
|
||
if not raw:
|
||
continue
|
||
if raw in ("?", "help"):
|
||
_show_inline_help()
|
||
continue
|
||
if raw in ("q", "quit", "exit"):
|
||
raise SystemExit(0)
|
||
|
||
try:
|
||
choice = int(raw)
|
||
except ValueError:
|
||
console.print("[error]⚠ Enter a number, ? for help, or q to quit.[/error]")
|
||
Prompt.ask("[dim]Press Enter to continue[/dim]", default="")
|
||
continue
|
||
|
||
if choice == 99:
|
||
return
|
||
elif choice == 98 and self.PROJECT_URL:
|
||
self.show_project_page()
|
||
elif 1 <= choice <= len(self.OPTIONS):
|
||
try:
|
||
self.OPTIONS[choice - 1][1]()
|
||
except Exception:
|
||
console.print_exception(show_locals=True)
|
||
Prompt.ask("[dim]Press Enter to continue[/dim]", default="")
|
||
else:
|
||
console.print("[error]⚠ Invalid option.[/error]")
|
||
|
||
def before_install(self): pass
|
||
|
||
def install(self):
|
||
self.before_install()
|
||
if isinstance(self.INSTALL_COMMANDS, (list, tuple)):
|
||
for cmd in self.INSTALL_COMMANDS:
|
||
console.print(f"[warning]→ {cmd}[/warning]")
|
||
os.system(cmd)
|
||
self.after_install()
|
||
|
||
def after_install(self):
|
||
console.print("[success]✔ Successfully installed![/success]")
|
||
|
||
def before_uninstall(self) -> bool:
|
||
return True
|
||
|
||
def uninstall(self):
|
||
if self.before_uninstall():
|
||
if isinstance(self.UNINSTALL_COMMANDS, (list, tuple)):
|
||
for cmd in self.UNINSTALL_COMMANDS:
|
||
console.print(f"[error]→ {cmd}[/error]")
|
||
os.system(cmd)
|
||
self.after_uninstall()
|
||
|
||
def after_uninstall(self): pass
|
||
|
||
def update(self):
|
||
"""Smart update — detects install method and runs the right update command."""
|
||
if not self.is_installed:
|
||
console.print("[warning]Tool is not installed yet. Install it first.[/warning]")
|
||
return
|
||
|
||
updated = False
|
||
for ic in (self.INSTALL_COMMANDS or []):
|
||
if "git clone" in ic:
|
||
# Extract repo dir name from clone command
|
||
parts = ic.split()
|
||
repo_urls = [p for p in parts if p.startswith("http")]
|
||
if repo_urls:
|
||
dirname = repo_urls[0].rstrip("/").rsplit("/", 1)[-1].replace(".git", "")
|
||
if os.path.isdir(dirname):
|
||
console.print(f"[cyan]→ git -C {dirname} pull[/cyan]")
|
||
os.system(f"git -C {dirname} pull")
|
||
updated = True
|
||
elif "pip install" in ic:
|
||
# Re-run pip install (--upgrade)
|
||
upgrade_cmd = ic.replace("pip install", "pip install --upgrade")
|
||
console.print(f"[cyan]→ {upgrade_cmd}[/cyan]")
|
||
os.system(upgrade_cmd)
|
||
updated = True
|
||
elif "go install" in ic:
|
||
# Re-run go install (fetches latest)
|
||
console.print(f"[cyan]→ {ic}[/cyan]")
|
||
os.system(ic)
|
||
updated = True
|
||
elif "gem install" in ic:
|
||
upgrade_cmd = ic.replace("gem install", "gem update")
|
||
console.print(f"[cyan]→ {upgrade_cmd}[/cyan]")
|
||
os.system(upgrade_cmd)
|
||
updated = True
|
||
|
||
if updated:
|
||
console.print("[success]✔ Update complete![/success]")
|
||
else:
|
||
console.print("[dim]No automatic update method available for this tool.[/dim]")
|
||
|
||
def _get_tool_dir(self) -> str | None:
|
||
"""Find the tool's local directory — clone target, pip location, or binary path."""
|
||
# 1. Check git clone target dir
|
||
for ic in (self.INSTALL_COMMANDS or []):
|
||
if "git clone" in ic:
|
||
parts = ic.split()
|
||
# If last arg is not a URL, it's a custom dir name
|
||
repo_urls = [p for p in parts if p.startswith("http")]
|
||
if repo_urls:
|
||
dirname = repo_urls[0].rstrip("/").rsplit("/", 1)[-1].replace(".git", "")
|
||
# Check custom target dir (arg after URL)
|
||
url_idx = parts.index(repo_urls[0])
|
||
if url_idx + 1 < len(parts):
|
||
dirname = parts[url_idx + 1]
|
||
if os.path.isdir(dirname):
|
||
return os.path.abspath(dirname)
|
||
|
||
# 2. Check binary location via which
|
||
if self.RUN_COMMANDS:
|
||
cmd = self.RUN_COMMANDS[0]
|
||
if "&&" in cmd:
|
||
# "cd foo && bar" → check "foo"
|
||
cd_part = cmd.split("&&")[0].strip()
|
||
if cd_part.startswith("cd "):
|
||
d = cd_part[3:].strip()
|
||
if os.path.isdir(d):
|
||
return os.path.abspath(d)
|
||
binary = cmd.split()[0] if cmd else ""
|
||
if binary.startswith("sudo"):
|
||
binary = cmd.split()[1] if len(cmd.split()) > 1 else ""
|
||
path = shutil.which(binary) if binary else None
|
||
if path:
|
||
return os.path.dirname(os.path.realpath(path))
|
||
|
||
return None
|
||
|
||
def open_folder(self):
|
||
"""Open the tool's directory in a new shell so the user can work manually."""
|
||
tool_dir = self._get_tool_dir()
|
||
if tool_dir:
|
||
console.print(f"[success]Opening folder: {tool_dir}[/success]")
|
||
console.print("[dim]Type 'exit' to return to hackingtool.[/dim]")
|
||
os.system(f'cd "{tool_dir}" && $SHELL')
|
||
else:
|
||
console.print("[warning]Tool directory not found.[/warning]")
|
||
if self.PROJECT_URL:
|
||
console.print(f"[dim]You can clone it manually:[/dim]")
|
||
console.print(f"[cyan] git clone {self.PROJECT_URL}.git[/cyan]")
|
||
|
||
def before_run(self): pass
|
||
|
||
def run(self):
|
||
self.before_run()
|
||
if isinstance(self.RUN_COMMANDS, (list, tuple)):
|
||
for cmd in self.RUN_COMMANDS:
|
||
console.print(f"[cyan]⚙ Running:[/cyan] [bold]{cmd}[/bold]")
|
||
os.system(cmd)
|
||
self.after_run()
|
||
|
||
def after_run(self): pass
|
||
|
||
def show_project_page(self):
|
||
console.print(f"[url]🌐 Opening: {self.PROJECT_URL}[/url]")
|
||
webbrowser.open_new_tab(self.PROJECT_URL)
|
||
|
||
|
||
class HackingToolsCollection:
|
||
TITLE: str = ""
|
||
DESCRIPTION: str = ""
|
||
TOOLS: list = []
|
||
|
||
def __init__(self):
|
||
pass
|
||
|
||
def show_info(self):
|
||
console.rule(f"[{THEME_PRIMARY}]{self.TITLE}[/{THEME_PRIMARY}]", style="purple")
|
||
if self.DESCRIPTION:
|
||
console.print(f"[italic cyan]{self.DESCRIPTION}[/italic cyan]\n")
|
||
|
||
def _active_tools(self) -> list:
|
||
"""Return tools that are not archived and are OS-compatible."""
|
||
from os_detect import CURRENT_OS
|
||
return [
|
||
t for t in self.TOOLS
|
||
if not getattr(t, "ARCHIVED", False)
|
||
and CURRENT_OS.system in getattr(t, "SUPPORTED_OS", ["linux", "macos"])
|
||
]
|
||
|
||
def _archived_tools(self) -> list:
|
||
return [t for t in self.TOOLS if getattr(t, "ARCHIVED", False)]
|
||
|
||
def _incompatible_tools(self) -> list:
|
||
from os_detect import CURRENT_OS
|
||
return [
|
||
t for t in self.TOOLS
|
||
if not getattr(t, "ARCHIVED", False)
|
||
and CURRENT_OS.system not in getattr(t, "SUPPORTED_OS", ["linux", "macos"])
|
||
]
|
||
|
||
def _show_archived_tools(self):
|
||
"""Show archived tools sub-menu (option 98)."""
|
||
archived = self._archived_tools()
|
||
if not archived:
|
||
console.print("[dim]No archived tools in this category.[/dim]")
|
||
Prompt.ask("[dim]Press Enter to return[/dim]", default="")
|
||
return
|
||
|
||
while True:
|
||
clear_screen()
|
||
console.rule(f"[archived]Archived Tools — {self.TITLE}[/archived]", style="yellow")
|
||
|
||
table = Table(box=box.MINIMAL_DOUBLE_HEAD, show_lines=True)
|
||
table.add_column("No.", justify="center", style="bold yellow")
|
||
table.add_column("Tool", style="dim yellow")
|
||
table.add_column("Reason", style="dim white")
|
||
|
||
for i, tool in enumerate(archived):
|
||
reason = getattr(tool, "ARCHIVED_REASON", "No reason given")
|
||
table.add_row(str(i + 1), tool.TITLE, reason)
|
||
|
||
table.add_row("99", "Back", "")
|
||
console.print(table)
|
||
|
||
raw = Prompt.ask("[bold yellow][?] Select[/bold yellow]", default="99")
|
||
try:
|
||
choice = int(raw)
|
||
except ValueError:
|
||
continue
|
||
|
||
if choice == 99:
|
||
return
|
||
elif 1 <= choice <= len(archived):
|
||
archived[choice - 1].show_options(parent=self)
|
||
|
||
def show_options(self, parent=None):
|
||
"""Iterative menu loop — no recursion, no stack growth."""
|
||
while True:
|
||
clear_screen()
|
||
self.show_info()
|
||
|
||
active = self._active_tools()
|
||
incompatible = self._incompatible_tools()
|
||
archived = self._archived_tools()
|
||
|
||
table = Table(title="Available Tools", box=box.SIMPLE_HEAD, show_lines=True)
|
||
table.add_column("No.", justify="center", style="bold cyan", width=6)
|
||
table.add_column("", width=2) # installed indicator
|
||
table.add_column("Tool", style="bold yellow", min_width=24)
|
||
table.add_column("Description", style="white", overflow="fold")
|
||
|
||
for index, tool in enumerate(active, start=1):
|
||
desc = getattr(tool, "DESCRIPTION", "") or "—"
|
||
desc = desc.splitlines()[0] if desc != "—" else "—"
|
||
has_status = hasattr(tool, "is_installed")
|
||
status = ("[green]✔[/green]" if tool.is_installed else "[dim]✘[/dim]") if has_status else ""
|
||
table.add_row(str(index), status, tool.TITLE, desc)
|
||
|
||
# Count not-installed tools for "Install All" label (skip sub-collections)
|
||
not_installed = [t for t in active if hasattr(t, "is_installed") and not t.is_installed]
|
||
if not_installed:
|
||
table.add_row(
|
||
"[bold green]97[/bold green]", "",
|
||
f"[bold green]Install all ({len(not_installed)} not installed)[/bold green]", "",
|
||
)
|
||
if archived:
|
||
table.add_row("[dim]98[/dim]", "", f"[archived]Archived tools ({len(archived)})[/archived]", "")
|
||
if incompatible:
|
||
console.print(f"[dim]({len(incompatible)} tools hidden — not supported on current OS)[/dim]")
|
||
|
||
table.add_row("99", "", f"Back to {parent.TITLE if parent else 'Main Menu'}", "")
|
||
console.print(table)
|
||
console.print(
|
||
" [dim cyan]?[/dim cyan][dim]help "
|
||
"[/dim][dim cyan]q[/dim cyan][dim]uit "
|
||
"[/dim][dim cyan]99[/dim cyan][dim] back[/dim]"
|
||
)
|
||
|
||
raw = Prompt.ask("[bold cyan]╰─>[/bold cyan]", default="").strip().lower()
|
||
if not raw:
|
||
continue
|
||
if raw in ("?", "help"):
|
||
_show_inline_help()
|
||
continue
|
||
if raw in ("q", "quit", "exit"):
|
||
raise SystemExit(0)
|
||
|
||
try:
|
||
choice = int(raw)
|
||
except ValueError:
|
||
console.print("[error]⚠ Enter a number, ? for help, or q to quit.[/error]")
|
||
continue
|
||
|
||
if choice == 99:
|
||
return
|
||
elif choice == 97 and not_installed:
|
||
console.print(Panel(
|
||
f"[bold]Installing {len(not_installed)} tools...[/bold]",
|
||
border_style="green", box=box.ROUNDED,
|
||
))
|
||
for i, tool in enumerate(not_installed, start=1):
|
||
console.print(f"\n[bold cyan]({i}/{len(not_installed)})[/bold cyan] {tool.TITLE}")
|
||
try:
|
||
tool.install()
|
||
except Exception:
|
||
console.print(f"[error]✘ Failed: {tool.TITLE}[/error]")
|
||
Prompt.ask("\n[dim]Press Enter to continue[/dim]", default="")
|
||
elif choice == 98 and archived:
|
||
self._show_archived_tools()
|
||
elif 1 <= choice <= len(active):
|
||
try:
|
||
active[choice - 1].show_options(parent=self)
|
||
except Exception:
|
||
console.print_exception(show_locals=True)
|
||
Prompt.ask("[dim]Press Enter to continue[/dim]", default="")
|
||
else:
|
||
console.print("[error]⚠ Invalid option.[/error]")
|