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

132 lines
4.7 KiB
Python

"""Tests for the inline REPL front-end (navigation only — no tool side effects)."""
import types
from hackingtool import repl
# ── Fakes ────────────────────────────────────────────────────────────────────
def _fake_tool(title):
t = types.SimpleNamespace(TITLE=title, opened=False)
t.show_options = lambda: setattr(t, "opened", True)
return t
class _Doc:
"""Minimal stand-in for a prompt_toolkit Document."""
def __init__(self, text):
self.text_before_cursor = text
# ── Completer ────────────────────────────────────────────────────────────────
def _complete(text, titles, tags):
comp = repl.HTCompleter(titles, tags)
return [c.text for c in comp.get_completions(_Doc(text), None)]
def test_completer_commands():
# Trailing space so completing closes the menu + readies the cursor for an arg.
out = _complete("/ta", ["Nmap"], ["web"])
assert out == ["/tags "]
def test_history_persists_to_file(tmp_path, monkeypatch):
"""↑↓ history is backed by a FileHistory at ~/.hackingtool/history."""
from prompt_toolkit.history import FileHistory
from hackingtool import prompt, constants
hist = tmp_path / "history"
monkeypatch.setattr(constants, "USER_HISTORY_FILE", hist)
prompt._session.cache_clear()
try:
session = prompt._session()
assert isinstance(session.history, FileHistory)
assert session.history.filename == str(hist)
finally:
prompt._session.cache_clear() # don't leak the tmp-bound session
def test_completer_tool_mention():
out = _complete("@nm", ["Nmap", "Nikto"], ["web"])
assert "@Nmap" in out and "@Nikto" not in out
def test_completer_tag_mention():
out = _complete("@tag:w", ["Nmap"], ["web", "wifi", "osint"])
assert set(out) == {"@tag:web", "@tag:wifi"}
def test_completer_no_completion_after_space():
# A bare goal ("scan the ...") must not trigger tool/command completion.
assert _complete("scan the ", ["Nmap"], ["web"]) == []
# ── _resolve ─────────────────────────────────────────────────────────────────
def test_resolve_case_insensitive():
tools = {"Nmap": _fake_tool("Nmap")}
assert repl._resolve("nmap", tools) is tools["Nmap"]
def test_resolve_fuzzy():
tools = {"Nmap": _fake_tool("Nmap")}
assert repl._resolve("nmpa", tools) is tools["Nmap"] # typo within 0.6 cutoff
def test_resolve_miss():
assert repl._resolve("wireshark", {"Nmap": _fake_tool("Nmap")}) is None
# ── dispatch (via the shared surface) ────────────────────────────────────────
def test_dispatch_quit_returns_false():
# Back-compat shim still used by test_skill; grammar now lives in prompt.
assert repl._dispatch("/quit") is False
def test_open_mention_opens_tool():
from hackingtool import prompt
tool = _fake_tool("Nmap")
prompt.open_mention("@nmap", tools_by_title={"Nmap": tool}, tag_index={})
assert tool.opened is True
def test_run_command_opens_tool():
from hackingtool import prompt
tool = _fake_tool("Nmap")
sig = prompt.dispatch("/run nmap", prompt.PromptCtx("home"))
assert sig == prompt.Open("@nmap")
prompt.open_mention(sig.mention, tools_by_title={"Nmap": tool}, tag_index={})
assert tool.opened is True
def test_dispatch_free_text_recommends(monkeypatch):
from hackingtool import prompt
seen = {}
import hackingtool.cli as cli
monkeypatch.setattr(cli, "recommend_tools", lambda intent=None: seen.setdefault("intent", intent))
prompt.dispatch("crack a wifi handshake", prompt.PromptCtx("home"))
assert seen["intent"] == "crack a wifi handshake"
def test_dispatch_tags_lists(monkeypatch):
from hackingtool import prompt
called = {}
monkeypatch.setattr(prompt, "show_all_tags", lambda idx: called.setdefault("hit", True))
prompt.dispatch("/tags", prompt.PromptCtx("home"))
assert called["hit"] is True
def test_find_is_completable_and_documented():
assert "/find" in repl._COMMANDS
def test_dispatch_find_routes_to_discover_run(monkeypatch):
from hackingtool import prompt
import hackingtool.discover as discover
seen = {}
monkeypatch.setattr(discover, "run", lambda need, ctx=None: seen.setdefault("need", need))
ctx = prompt.PromptCtx("home")
prompt.dispatch("/find crack a hash", ctx)
assert seen["need"] == "crack a hash"