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

105 lines
4.0 KiB
Python

#!/usr/bin/env python3
"""
Apply M1 archive verdicts to tools/*.py.
Reads scripts/tool_audit.json and, for every tool classified DEAD / ARCHIVE / STALE
(and not already archived), injects `ARCHIVED = True` + `ARCHIVED_REASON = "..."`
as the first statements of that tool's class body. Idempotent; skips classes that
already carry ARCHIVED. Run with --apply to write; default is a dry run.
Usage:
python3 scripts/apply_archive.py # dry run (shows diff plan)
python3 scripts/apply_archive.py --apply
"""
from __future__ import annotations
import argparse
import json
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
AUDIT = ROOT / "scripts" / "tool_audit.json"
TARGET_VERDICTS = {"DEAD", "ARCHIVE", "STALE"}
def reason_for(rec: dict) -> str:
v, pushed = rec["verdict"], (rec.get("pushed_at") or "")[:7]
if v == "DEAD":
if "blocked" in rec["reason"]:
return "Upstream repo removed/blocked (DMCA or suspended)"
return "Upstream repo deleted (404)"
if v == "ARCHIVE":
return f"Upstream repo archived by author{f' (last commit {pushed})' if pushed else ''}"
return f"Unmaintained — no commits since {pushed}" if pushed else "Unmaintained upstream"
def apply_to_file(path: Path, targets: list[dict], write: bool) -> list[str]:
"""Insert ARCHIVED lines for each target class in this file. Returns log lines."""
lines = path.read_text(encoding="utf-8").splitlines(keepends=True)
log = []
# Descend by class-line index so insertions don't shift later ones.
plans = []
for rec in targets:
cname = rec["class"]
idx = next((i for i, ln in enumerate(lines)
if re.match(rf"^class\s+{re.escape(cname)}\s*\(", ln)), None)
if idx is None:
log.append(f" ?? {cname}: class not found in {path.name} — skipped")
continue
# Find class-body extent (until next top-level `class ` or EOF).
end = next((j for j in range(idx + 1, len(lines))
if re.match(r"^class\s+\w+\s*\(", lines[j])), len(lines))
block = "".join(lines[idx:end])
if re.search(r"^\s+ARCHIVED\s*=\s*True", block, re.M):
log.append(f" == {cname}: already ARCHIVED — skipped")
continue
# Body indent = leading spaces of the first non-blank body line (default 4).
indent = " "
for j in range(idx + 1, end):
if lines[j].strip():
indent = lines[j][:len(lines[j]) - len(lines[j].lstrip())]
break
reason = reason_for(rec).replace('"', "'")
ins = f"{indent}ARCHIVED = True\n{indent}ARCHIVED_REASON = \"{reason}\"\n"
plans.append((idx, cname, rec["verdict"], reason, ins))
for idx, cname, verdict, reason, ins in sorted(plans, key=lambda p: p[0], reverse=True):
lines.insert(idx + 1, ins)
log.append(f" ++ {cname} [{verdict}] → {reason}")
if write and plans:
path.write_text("".join(lines), encoding="utf-8")
return log
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--apply", action="store_true")
args = ap.parse_args()
records = json.loads(AUDIT.read_text())
targets = [r for r in records
if r["verdict"] in TARGET_VERDICTS and not r["already_archived"]]
by_file: dict[str, list[dict]] = {}
for r in targets:
by_file.setdefault(r["file"], []).append(r)
total = 0
print(f"{'APPLYING' if args.apply else 'DRY RUN'} — {len(targets)} classes across {len(by_file)} files\n")
for f in sorted(by_file):
log = apply_to_file(ROOT / f, by_file[f], args.apply)
applied = [l for l in log if l.strip().startswith("++")]
total += len(applied)
print(f"{f} ({len(applied)} archived)")
for l in log:
print(l)
print(f"\n{'Wrote' if args.apply else 'Would archive'} {total} classes."
+ ("" if args.apply else " Re-run with --apply to write."))
return 0
if __name__ == "__main__":
raise SystemExit(main())