#!/usr/bin/env python3
"""Cosecha el manual de las herramientas que no traen man page.

    ./manuales.py            regenera conocimiento/09 - Manuales/
    ./manuales.py --lista    di que herramientas cosecharia y cuales estan

Muchas herramientas de pentesting (ffuf, sqlmap, la suite impacket, netexec...)
no instalan man page: su "manual" es lo que sueltan con --help. Este script lo
captura, lo limpia de rutas y datos de la maquina, y lo escribe como markdown en
el pack de conocimiento, donde indexa.py lo recoge como todo lo demas.

Solo cosecha lo que ESTA instalado en la maquina. El resultado (los .md) se
commitea, asi que crece con lo que cada quien tenga: quien instale una
herramienta mas, regenera y manda el .md nuevo. Es el complemento verificado del
cheatsheet escrito a mano (que da las invocaciones comunes; esto da la lista
completa de opciones).
"""
import argparse
import os
import re
import shutil
import subprocess
import sys

AQUI = os.path.dirname(os.path.abspath(__file__))
DESTINO = os.path.join(AQUI, "conocimiento", "09 - Manuales")

# Las herramientas cuyo manual vale la pena y que no suelen traer man page.
# (comando, [argumentos para pedir la ayuda]). Se prueban en orden hasta que una
# devuelva algo con sustancia.
CATALOGO = [
    ("ffuf", ["-h"]),
    ("feroxbuster", ["--help"]),
    ("gobuster", ["--help"]),
    ("sqlmap", ["-hh"]),
    ("nuclei", ["-h"]),
    ("hydra", ["-h"]),
    ("netexec", ["--help"]),
    ("crackmapexec", ["--help"]),
    ("evil-winrm", ["--help"]),
    ("john", []),               # john sin args ya imprime el uso
    ("hashcat", ["--help"]),
    ("wpscan", ["--help"]),
    ("nikto", ["-Help"]),
    ("smbmap", ["-h"]),
    ("enum4linux-ng", ["-h"]),
    ("wfuzz", ["-h"]),
    ("dirsearch", ["-h"]),
    ("responder", ["-h"]),
    ("chisel", ["--help"]),
    ("ligolo-proxy", ["-h"]),
    ("bloodhound-python", ["-h"]),
    ("certipy", ["-h"]),
    ("kerbrute", ["-h"]),
    # La suite impacket: cada binario es una herramienta.
    ("impacket-secretsdump", ["-h"]),
    ("impacket-GetUserSPNs", ["-h"]),
    ("impacket-GetNPUsers", ["-h"]),
    ("impacket-getTGT", ["-h"]),
    ("impacket-psexec", ["-h"]),
    ("impacket-wmiexec", ["-h"]),
    ("impacket-smbexec", ["-h"]),
    ("impacket-smbserver", ["-h"]),
    ("impacket-mssqlclient", ["-h"]),
    ("impacket-ntlmrelayx", ["-h"]),
]

ANSI = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
CASA = re.compile(re.escape(os.path.expanduser("~")))
USUARIO = re.compile(re.escape(os.environ.get("USER", "\0nadie\0")))


def limpia(texto):
    """Quita colores y cualquier rastro de la maquina de esta cosecha."""
    texto = ANSI.sub("", texto)
    texto = CASA.sub("~", texto)                       # /home/quien-sea -> ~
    texto = USUARIO.sub("usuario", texto)              # el login real -> generico
    texto = re.sub(r"[ \t]+\n", "\n", texto)           # espacios al final de linea
    texto = re.sub(r"\n{3,}", "\n\n", texto)           # como mucho una linea en blanco
    return texto.strip()


def ayuda(cmd, args):
    """La salida de ayuda de una herramienta, o None si no dice nada util."""
    if not shutil.which(cmd):
        return None
    for intento in (args, ["--help"], ["-h"], []):
        try:
            r = subprocess.run([cmd, *intento], capture_output=True, text=True,
                               timeout=15, errors="ignore")
        except (subprocess.TimeoutExpired, OSError):
            continue
        salida = (r.stdout or "") + (r.stderr or "")
        salida = limpia(salida)
        if len(salida) >= 120:                         # menos que esto no es un manual
            return salida
    return None


def escribe(cmd, texto):
    md = (f"# {cmd} — manual\n\n"
          f"Salida de `{cmd} {'--help' if not cmd.startswith('impacket') else '-h'}`, "
          f"capturada de la propia herramienta. Referencia completa de opciones "
          f"(las invocaciones del dia a dia estan en el cheatsheet de comandos).\n\n"
          f"```\n{texto}\n```\n")
    os.makedirs(DESTINO, exist_ok=True)
    ruta = os.path.join(DESTINO, f"{cmd}.md")
    with open(ruta, "w", encoding="utf-8") as f:
        f.write(md)
    return ruta


def main():
    p = argparse.ArgumentParser()
    p.add_argument("--lista", action="store_true",
                   help="di que cosecharia y que hay instalado, sin escribir")
    a = p.parse_args()

    if a.lista:
        for cmd, _ in CATALOGO:
            hay = "instalada" if shutil.which(cmd) else "-"
            print(f"  {cmd:26s} {hay}")
        return 0

    hechos = faltan = 0
    for cmd, args in CATALOGO:
        texto = ayuda(cmd, args)
        if texto is None:
            faltan += 1
            continue
        ruta = escribe(cmd, texto)
        print(f"  {cmd:26s} -> {os.path.relpath(ruta, AQUI)} ({len(texto)} car.)")
        hechos += 1
    print(f"\n  {hechos} manuales cosechados, {faltan} no instalados en esta maquina")
    return 0


if __name__ == "__main__":
    sys.exit(main())
