Files
Hardik Zinzuvadiya 2482cc9fd0 feat: AI operator console — 215 curated tools, AI layer, /find discovery (v2.0.0 rework)
Squashed rework of hackingtool from a tool launcher into an AI-guided operator
console for authorized security testing. 93 commits collapsed into this one;
the pre-rework tree is tagged v2.0.0.

CATALOG & ENGINE
- Data-driven YAML catalog (21 categories, 215 live tools + 59 archived) with a
  registry/overlay loader and a fixed 66-tag taxonomy (63 in use). Adding a tool
  is one YAML entry, not edits across the codebase.
- Engine honesty: real exit codes, truthful install success/failure, reuse-first
  skip, EOF-safe prompts, command audit logging.
- Safe installs: sha256-required safe-fetch (killed `curl | bash` in feroxbuster,
  Caido and Sliver), list-form subprocess only, no forced sudo.

AI LAYER (bring-your-own-key or local model; degrades offline, never fabricates)
- AI1 intent -> tools; AI2 tool+goal -> command, curated-first with a grounded
  fallback; AI3 findings summary and engagement report; AI4 per-finding impact
  and remediation. Prompt-injection hardened per OWASP LLM01.
- /goal plans an objective and runs it one step at a time, showing every command
  before it runs, with a plan.json + run.log audit trail.

/find TOOL DISCOVERY (this branch's headline feature)
- Suggests real GitHub projects when the catalog has no tool for a need.
  Deterministic: zero model calls, structured API fields only, suggest-only —
  it never clones, installs or runs anything.
- A charter filter refuses destructive/DoS/jamming/mass-targeting/evasion asks
  before any network I/O, while a defensive-intent guard keeps blue-team and
  DFIR phrasing ("detect a SYN flood in a pcap") from being false-refused.
- Query rewriting proved to be the dominant quality lever (the first design
  measured 29% precision with no results on 5 of 8 needs): a curated 41-row
  intent table maps plain English to canonical jargon plus a GitHub topic, and
  a two-arm search unions topic coverage with jargon precision.
- Explainable additive ranking: log-flattened stars, license/age/language,
  trusted-author bonus derived from owners we already ship, docs-repo demotion
  by name, staleness as a soft demotion rather than a filter (a hard cutoff
  would delete THC-Hydan and John the Ripper), and a relevance term weighting
  curated topics above free-text description.
- Optional no-scope GitHub token purely as a rate-limit lever (10 -> 30 req/min);
  it reaches only an Authorization header, never a cache key, log or output.
- `[a]` saves a pick to ~/.hackingtool/found.yaml as a structurally inert entry,
  and the loader strips executable keys from user catalogs at read time so a
  hand-edited file cannot become a runnable command.

CONSOLE & PACKAGING
- REPL with a / command palette, @ tool mentions, tag filters, history and
  completion; background tmux panes; settings and first-run scaffolding.
- src-layout package with catalog and pipelines as package data, console entry
  point, Docker image, signed releases with SBOM and build provenance.
- Health docs (SECURITY, CONTRIBUTING, CHANGELOG, CODE_OF_CONDUCT), a CI gate
  (ruff + pytest + catalog/taxonomy conformance) and a pre-push hook.
- README rewritten with a section index, the tool catalog split into
  docs/TOOLS.md and a step-by-step docs/HOW-TO-USE.md.

278 tests passing; scripts/check.sh green.
2026-07-27 00:18:34 +05:30

155 lines
5.3 KiB
Python

"""Engine-honesty checks for core.py (M4): exit-code capture, reuse-first skip,
honest success/failure, and EOF-safe prompting."""
import pytest
import hackingtool.core as core
def _tool(cmds):
t = core.HackingTool(installable=True, runnable=False)
t.TITLE = "T"
t.INSTALL_COMMANDS = cmds
t.SYSTEM_PKGS = {}
return t
def test_install_stops_on_failure_and_is_honest(monkeypatch):
run = []
codes = {"a": 0, "b": 1, "c": 0}
monkeypatch.setattr(core, "_run_shell", lambda c: run.append(c) or codes[c])
t = _tool(["a", "b", "c"])
after = []
monkeypatch.setattr(t, "after_install", lambda: after.append(1))
t.install()
assert run == ["a", "b"] # stopped at the first failing command
assert after == [] # did NOT claim success on failure
def test_install_success_calls_after(monkeypatch):
run = []
monkeypatch.setattr(core, "_run_shell", lambda c: run.append(c) or 0)
t = _tool(["a", "b"])
after = []
monkeypatch.setattr(t, "after_install", lambda: after.append(1))
t.install()
assert run == ["a", "b"]
assert after == [1]
def test_reuse_first_skips_install(monkeypatch):
ran = []
monkeypatch.setattr(core, "_run_shell", lambda c: ran.append(c) or 0)
t = _tool(["git clone https://x/y.git"])
monkeypatch.setattr(t, "_already_present", lambda: True)
t.install()
assert ran == [] # already present → nothing executed
def test_already_present_uses_which_not_interpreter(monkeypatch):
# A python3-launched tool must NOT read as present just because python3 is on PATH.
t = _tool(["git clone https://x/notcloned.git"])
t.RUN_COMMANDS = ["python3 main.py"]
monkeypatch.setattr(core.shutil, "which", lambda b: "/usr/bin/python3")
assert t._already_present() is False
# But an explicit system binary is trusted.
t.SYSTEM_PKGS = {"which": "nmap"}
assert t._already_present() is True
class _FakeResp:
def __init__(self, data):
self._d = data
def __enter__(self):
return self
def __exit__(self, *a):
return False
def read(self):
return self._d
def _url_tool(sha, cmds=None):
t = core.HackingTool(installable=True, runnable=False)
t.TITLE = "U"
t.INSTALL_URL = "https://x/bin"
t.INSTALL_SHA256 = sha
t.INSTALL_COMMANDS = cmds if cmds is not None else []
t.SYSTEM_PKGS = {}
return t
def test_url_install_refuses_when_unpinned(monkeypatch):
ran = []
monkeypatch.setattr(core, "_run_shell", lambda c: ran.append(c) or 0)
monkeypatch.setattr("urllib.request.urlopen", lambda u: _FakeResp(b"data"))
_url_tool("").install()
assert ran == [] # no sha256 → nothing runs
def test_url_install_refuses_on_mismatch(monkeypatch):
ran = []
monkeypatch.setattr(core, "_run_shell", lambda c: ran.append(c) or 0)
monkeypatch.setattr("urllib.request.urlopen", lambda u: _FakeResp(b"data"))
_url_tool("deadbeef" * 8).install() # 64-hex but wrong
assert ran == [] # checksum mismatch → nothing runs
def test_url_install_runs_on_match_with_substitution(monkeypatch):
import hashlib
data = b"payload"
good = hashlib.sha256(data).hexdigest()
ran = []
monkeypatch.setattr(core, "_run_shell", lambda c: ran.append(c) or 0)
monkeypatch.setattr("urllib.request.urlopen", lambda u: _FakeResp(data))
_url_tool(good, ["chmod +x {file}", "mv {file} /dest"]).install()
assert len(ran) == 2
assert all("{file}" not in c for c in ran) # placeholder substituted
assert ran[0].startswith("chmod +x /") and ran[1].endswith("/dest")
def test_run_shell_logs_command_and_exit_code(monkeypatch):
logged = []
fake = type("L", (), {"info": lambda self, m, *a: logged.append(m % a)})()
monkeypatch.setattr(core, "_cmd_logger", lambda: fake)
assert core._run_shell("exit 3") == 3
assert logged == ["exit=3 :: exit 3"]
def test_ask_quits_on_eof(monkeypatch):
# ask now delegates to prompt.simple, which reads via input(); EOF still
# cleanly quits (SystemExit) instead of crashing the menu loop.
def boom(*a, **k):
raise EOFError()
monkeypatch.setattr("builtins.input", boom)
with pytest.raises(SystemExit):
core.ask("prompt")
@pytest.mark.parametrize("url", [
"javascript:alert(1)", "file:///etc/passwd", "data:text/html,<script>",
"ftp://example.com", " javascript:alert(1)",
])
def test_project_page_refuses_non_http_urls(monkeypatch, url):
"""PROJECT_URL used to come only from the vetted catalog; /find now also
writes it into ~/.hackingtool/found.yaml from GitHub data, so a hand-edited
file must not be able to hand webbrowser a javascript:/file:/data: URL."""
opened = []
monkeypatch.setattr(core.webbrowser, "open_new_tab", opened.append)
t = core.HackingTool(installable=False, runnable=False)
t.TITLE = "T"
t.PROJECT_URL = url
t.show_project_page()
assert opened == [], f"{url!r} must never reach the browser"
def test_project_page_still_opens_normal_links(monkeypatch):
opened = []
monkeypatch.setattr(core.webbrowser, "open_new_tab", opened.append)
t = core.HackingTool(installable=False, runnable=False)
t.TITLE = "T"
t.PROJECT_URL = "https://github.com/ffuf/ffuf"
t.show_project_page()
assert opened == ["https://github.com/ffuf/ffuf"]