mirror of
https://github.com/Z4nzu/hackingtool.git
synced 2026-11-04 15:00:08 +00:00
Interactive terminal UI (Textual, default on a TTY) - Category browser with live tool counts; type to search EVERY tool across all categories; up/down move, Enter open, left/Esc back; `/` opens a command palette. - Tool screen gates Run/Install/Uninstall behind a confirm, dropping to a real TTY so a tool can't crash the UI; Open-repo is http(s)-guarded. - The engine is unchanged; `--classic` keeps the full prompt_toolkit REPL and the headless `--engagement` orchestrator is untouched. Update awareness + self-update helper - Cached-daily GitHub Releases check (offline-safe, opt-out via HACKINGTOOL_NO_UPDATE_CHECK / update_check config); shows an "update available" line on the main screen via a background worker (never blocks first render). - `/update` detects the install method (pipx/pip/apt/docker/source) and shows the exact upgrade command to copy - it never runs it. Native settings + config hygiene - `/update`/`/config` run as in-TUI screens instead of suspending to prompt_toolkit (which raised "event loop already running" nested under Textual). - Sponsor feed URL moved from user config to a fixed code constant (SPONSORS_URL); update_check is now a proper bool toggle. Docker / README - Docker image references aligned to hardikzinzu/hackingtool. - README refreshed for the interactive UI; first screenshot replaced with the new UI. Community-reported bug fixes (verified still present in current code) - Data safety: atomic JSON/YAML writes (#773, #765); malformed config.json no longer crashes load/import (#775); UTF-8 for catalog/settings IO (#766); findings parsers skip non-object/typed records instead of aborting (#768); pipeline decodes invalid bytes and salvages timed-out evidence (#771, #770). - AI layer: reject null/non-text provider responses (#778); auto-mode connection test honors the Ollama fallback (#779); settings stay responsive during the AI probe (#780); one findings snapshot per AI report (#772). - CLI/REPL: `@nmap`/`@sqlmap` resolve canonically instead of fuzzy mis-hits (#767); bare `/` no longer crashes the REPL (#761); bad/incomplete args exit non-zero instead of opening the menu (#763); search/tag result #99 is reachable (#762); background pane creation failures are reported, not shown as "started" (#777); config/API-key save failures surface without closing settings (#776). - Misc: .dockerignore keeps README.md for the build (#781); PortScan prompt markup fixed (#698); uninstall command lines use [warning] not [error] (#666, #663); untrusted-output fence can't be broken out of + script import fix (#757).
137 lines
4.7 KiB
Python
137 lines
4.7 KiB
Python
import subprocess
|
|
|
|
import pytest
|
|
|
|
from hackingtool import session
|
|
|
|
|
|
class _Rec:
|
|
"""Records tmux argv; returns a scripted CompletedProcess."""
|
|
def __init__(self):
|
|
self.calls = []
|
|
self.returncode = 0
|
|
self.stdout = ""
|
|
|
|
def __call__(self, args, capture_output=False, text=True, check=False):
|
|
args_list = list(args)
|
|
# Record just the tmux argv (strip "tmux" prefix)
|
|
if args_list and args_list[0] == "tmux":
|
|
args_list = args_list[1:]
|
|
self.calls.append(args_list)
|
|
return subprocess.CompletedProcess(args, self.returncode, self.stdout, "")
|
|
|
|
|
|
@pytest.fixture
|
|
def rec(monkeypatch):
|
|
r = _Rec()
|
|
monkeypatch.setattr(session.subprocess, "run", r)
|
|
return r
|
|
|
|
|
|
def test_available_true(monkeypatch):
|
|
monkeypatch.setattr(session.shutil, "which", lambda _: "/usr/bin/tmux")
|
|
assert session.available() is True
|
|
|
|
|
|
def test_available_false(monkeypatch):
|
|
monkeypatch.setattr(session.shutil, "which", lambda _: None)
|
|
assert session.available() is False
|
|
|
|
|
|
def test_enabled_auto_present(monkeypatch):
|
|
from hackingtool import config
|
|
monkeypatch.setattr(session, "available", lambda: True)
|
|
monkeypatch.setattr(config, "load", lambda: {"background_runner": "auto"})
|
|
assert session.enabled() is True
|
|
|
|
|
|
def test_enabled_auto_absent(monkeypatch):
|
|
from hackingtool import config
|
|
monkeypatch.setattr(session, "available", lambda: False)
|
|
monkeypatch.setattr(config, "load", lambda: {"background_runner": "auto"})
|
|
assert session.enabled() is False
|
|
|
|
|
|
def test_enabled_off(monkeypatch):
|
|
from hackingtool import config
|
|
monkeypatch.setattr(session, "available", lambda: True)
|
|
monkeypatch.setattr(config, "load", lambda: {"background_runner": "off"})
|
|
assert session.enabled() is False
|
|
|
|
|
|
def test_run_creates_session_when_absent(rec, monkeypatch):
|
|
monkeypatch.setattr(session, "_has_session", lambda: False)
|
|
monkeypatch.setattr(session, "windows", lambda: [])
|
|
label = session.run("nmap", "/tools/nmap", command="nmap -sV h", banner="scan")
|
|
assert label == "nmap"
|
|
assert ["new-session", "-d", "-s", "hackingtool", "-n", "nmap",
|
|
"-c", "/tools/nmap"] in rec.calls
|
|
assert ["send-keys", "-t", "hackingtool:nmap", "# scan", "Enter"] in rec.calls
|
|
assert ["send-keys", "-t", "hackingtool:nmap", "nmap -sV h", "Enter"] in rec.calls
|
|
|
|
|
|
def test_run_adds_window_when_present(rec, monkeypatch):
|
|
monkeypatch.setattr(session, "_has_session", lambda: True)
|
|
monkeypatch.setattr(session, "windows", lambda: [])
|
|
session.run("nuclei", "/tools/nuclei")
|
|
assert ["new-window", "-t", "hackingtool", "-n", "nuclei",
|
|
"-c", "/tools/nuclei"] in rec.calls
|
|
# bare tool (no command) → no command send-keys
|
|
assert not any(c[:1] == ["send-keys"] and c[-2] == "nuclei" for c in rec.calls)
|
|
|
|
|
|
def test_run_dedups_label(rec, monkeypatch):
|
|
monkeypatch.setattr(session, "_has_session", lambda: True)
|
|
monkeypatch.setattr(session, "windows", lambda: [(0, "nmap")])
|
|
assert session.run("nmap", "/tools/nmap") == "nmap-2"
|
|
|
|
|
|
def test_windows_parses(monkeypatch):
|
|
monkeypatch.setattr(session, "available", lambda: True)
|
|
|
|
def fake_run(args, capture_output=False, text=True, check=False):
|
|
return subprocess.CompletedProcess(args, 0, "0:nmap\n1:nuclei\n", "")
|
|
|
|
monkeypatch.setattr(session.subprocess, "run", fake_run)
|
|
assert session.windows() == [(0, "nmap"), (1, "nuclei")]
|
|
|
|
|
|
def test_windows_no_server(monkeypatch):
|
|
monkeypatch.setattr(session, "available", lambda: True)
|
|
|
|
def fake_run(args, capture_output=False, text=True, check=False):
|
|
return subprocess.CompletedProcess(args, 1, "", "no server running")
|
|
|
|
monkeypatch.setattr(session.subprocess, "run", fake_run)
|
|
assert session.windows() == []
|
|
|
|
|
|
def test_windows_absent_tmux(monkeypatch):
|
|
monkeypatch.setattr(session, "available", lambda: False)
|
|
assert session.windows() == []
|
|
|
|
|
|
def test_kill_all(rec):
|
|
session.kill("all")
|
|
assert ["kill-session", "-t", "hackingtool"] in rec.calls
|
|
|
|
|
|
def test_kill_window(rec):
|
|
session.kill("nmap")
|
|
assert ["kill-window", "-t", "hackingtool:nmap"] in rec.calls
|
|
|
|
|
|
def test_no_crash_when_tmux_binary_missing(monkeypatch):
|
|
"""Verify session functions degrade gracefully when tmux binary is absent."""
|
|
import pytest
|
|
def boom(*a, **k):
|
|
raise FileNotFoundError("tmux")
|
|
monkeypatch.setattr(session.subprocess, "run", boom)
|
|
monkeypatch.setattr(session, "windows", lambda: [])
|
|
# cleanup helpers still degrade silently
|
|
session.kill("nmap")
|
|
session.kill("all")
|
|
# run() now SIGNALS the failure (caller reports it) instead of pretending it started
|
|
with pytest.raises(session.SessionError):
|
|
session.run("x", "/tmp")
|