Commit 61690a42 authored by Hardik Zinzuvadiya's avatar Hardik Zinzuvadiya
Browse files

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
parent 26e38aea
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -92,6 +92,9 @@ class HackingTool:
    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    = ""
+95 −2
Original line number Diff line number Diff line
@@ -122,6 +122,7 @@ def show_help():
            ("  1–20   ", "bold cyan"), ("open a category\n", "white"),
            ("  21     ", "bold cyan"), ("Update / Uninstall hackingtool\n", "white"),
            ("  / or s ", "bold cyan"), ("search tools by name or keyword\n", "white"),
            ("  t      ", "bold cyan"), ("filter tools by tag (osint, web, c2, ...)\n", "white"),
            ("  ?      ", "bold cyan"), ("show this help\n", "white"),
            ("  q      ", "bold cyan"), ("quit hackingtool\n\n", "white"),
            ("  Inside a category\n", "bold white"),
@@ -317,6 +318,7 @@ def build_menu():
    console.print(
        "  [dim]Enter number  ·  "
        "[bold cyan]/[/bold cyan] search  ·  "
        "[bold cyan]t[/bold cyan] tags  ·  "
        "[bold cyan]?[/bold cyan] help  ·  "
        "[bold cyan]q[/bold cyan] quit[/dim]\n"
    )
@@ -340,6 +342,92 @@ def _collect_all_tools() -> list[tuple]:
    return results


def _get_all_tags() -> dict[str, list[tuple]]:
    """Build tag → [(tool, category)] index from all tools."""
    import re
    _rules = {
        r'(osint|harvester|maigret|holehe|spiderfoot|sherlock|recon)': 'osint',
        r'(subdomain|subfinder|amass|sublist|subdomainfinder)': 'recon',
        r'(scanner|scan|nmap|masscan|rustscan|nikto|nuclei|trivy)': 'scanner',
        r'(brute|gobuster|ffuf|dirb|dirsearch|ferox|hashcat|john|kerbrute)': 'bruteforce',
        r'(web|http|proxy|zap|xss|sql|wafw00f|arjun|caido|mitmproxy)': 'web',
        r'(wireless|wifi|wlan|airgeddon|bettercap|wifite|fluxion|deauth)': 'wireless',
        r'(phish|social.media|evilginx|setoolkit|social.fish|social.engineer)': 'social-engineering',
        r'(c2|sliver|havoc|mythic|pwncat|reverse.shell|pyshell)': 'c2',
        r'(privesc|peass|linpeas|winpeas)': 'privesc',
        r'(tunnel|pivot|ligolo|chisel|proxy|anon)': 'network',
        r'(password|credential|hash|crack|secret|trufflehog|gitleaks)': 'credentials',
        r'(forensic|memory|volatility|binwalk|autopsy|wireshark|pspy)': 'forensics',
        r'(reverse.eng|ghidra|radare|jadx|androguard|apk)': 'reversing',
        r'(cloud|aws|azure|gcp|kubernetes|prowler|scout|pacu)': 'cloud',
        r'(mobile|android|ios|frida|mobsf|objection|droid)': 'mobile',
        r'(active.directory|bloodhound|netexec|impacket|responder|certipy|kerberos|winrm|smb|ldap)': 'active-directory',
        r'(ddos|dos|slowloris|goldeneye|ufonet)': 'ddos',
        r'(payload|msfvenom|fatrat|venom|stitch|enigma)': 'payload',
        r'(crawler|spider|katana|gospider)': 'crawler',
    }
    tag_index: dict[str, list[tuple]] = {}
    for tool, cat in _collect_all_tools():
        combined = f"{tool.TITLE} {tool.DESCRIPTION}".lower()
        # Manual tags first
        tool_tags = set(getattr(tool, "TAGS", []) or [])
        # Auto-derive tags from title/description
        for pattern, tag in _rules.items():
            if re.search(pattern, combined, re.IGNORECASE):
                tool_tags.add(tag)
        for t in tool_tags:
            tag_index.setdefault(t, []).append((tool, cat))
    return tag_index


def filter_by_tag():
    """Show available tags, user picks one, show matching tools."""
    tag_index = _get_all_tags()
    sorted_tags = sorted(tag_index.keys())

    # Show tags in a compact grid
    console.print(Panel(
        "  ".join(f"[bold cyan]{t}[/bold cyan]([dim]{len(tag_index[t])}[/dim])" for t in sorted_tags),
        title="[bold magenta] Available Tags [/bold magenta]",
        border_style="magenta", box=box.ROUNDED, padding=(0, 2),
    ))

    tag = Prompt.ask("[bold cyan]Enter tag[/bold cyan]", default="").strip().lower()
    if not tag or tag not in tag_index:
        if tag:
            console.print(f"[dim]Tag '{tag}' not found.[/dim]")
            Prompt.ask("[dim]Press Enter to return[/dim]", default="")
        return

    matches = tag_index[tag]
    table = Table(
        title=f"Tools tagged '{tag}'",
        box=box.SIMPLE_HEAD, show_lines=True,
    )
    table.add_column("No.", justify="center", style="bold cyan", width=5)
    table.add_column("", width=2)
    table.add_column("Tool", style="bold yellow", min_width=20)
    table.add_column("Category", style="magenta", min_width=15)

    for i, (tool, cat) in enumerate(matches, start=1):
        status = "[green]✔[/green]" if tool.is_installed else "[dim]✘[/dim]"
        table.add_row(str(i), status, tool.TITLE, cat)

    table.add_row("99", "", "Back to main menu", "")
    console.print(table)

    raw = Prompt.ask("[bold cyan]>[/bold cyan]", default="").strip()
    if not raw or raw == "99":
        return
    try:
        idx = int(raw)
    except ValueError:
        return
    if 1 <= idx <= len(matches):
        tool, cat = matches[idx - 1]
        tool.show_options()


def search_tools():
    """Interactive search — user types query, results update, select to jump."""
    query = Prompt.ask("[bold cyan]/ Search[/bold cyan]", default="").strip().lower()
@@ -348,12 +436,13 @@ def search_tools():

    all_tool_list = _collect_all_tools()

    # Match against title + description
    # Match against title + description + tags
    matches = []
    for tool, category in all_tool_list:
        title = (tool.TITLE or "").lower()
        desc = (tool.DESCRIPTION or "").lower()
        if query in title or query in desc:
        tags = " ".join(getattr(tool, "TAGS", []) or []).lower()
        if query in title or query in desc or query in tags:
            matches.append((tool, category))

    if not matches:
@@ -415,6 +504,10 @@ def interact_menu():
                search_tools()
                continue

            if raw in ("t", "tag", "tags", "filter"):
                filter_by_tag()
                continue

            if raw in ("q", "quit", "exit"):
                console.print(Panel(
                    "[bold white on magenta]  Goodbye — Come Back Safely  [/bold white on magenta]",