JARVIS/nucleo/saber/manuales.py
sito 9fa0a40edf saber: manuales cosechados e indice publico pre-construido
- manuales.py cosecha el --help de las herramientas que no traen man page
  (ffuf, sqlmap, suite impacket, netexec, hydra...) y lo escribe como markdown
  en conocimiento/09 - Manuales/. Es la referencia de opciones que la man page
  no da; complementa el cheatsheet de invocaciones comunes.
- indexa.py --publico construye saber-publico.jsonl: glosario + pack de
  conocimiento con los vectores ya calculados, 100% publico y portable.
- busca.py usa ese indice publico como fallback cuando no hay indice personal,
  asi un clon recien hecho consulta al instante sin reindexar nada.
- .gitignore deja pasar saber-publico.jsonl; el personal (saber.jsonl) sigue
  fuera.
- docs/rag.md y el README del pack documentan ambas cosas.
2026-08-16 18:43:56 +02:00

139 lines
4.9 KiB
Python

#!/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())