saber: lo instalado en la maquina y los docs de repos clonados entran al indice
instalado.py lee los .desktop y los paquetes para saber que hay en el equipo y para que sirve cada cosa. indexa.py lo engancha, y con el los docs markdown de las herramientas clonadas en CODERS. Las dos fuentes son personales: se quedan en el indice de la maquina y no entran en el publico. busca_cli.py expone el mismo motor que usa el cerebro (saber.busca) para consultar el indice desde la terminal, con --n y --json. reentrena_noche.sh encadena las fases del reentrenamiento largo y las deja corriendo horas; cada fase es reanudable por su cuenta. eval del indice vivo: hit@1 43/57 (75%), hit@5 55/57 (96%), sim 0.909.
This commit is contained in:
parent
b1f8118272
commit
7c5381eea7
7 changed files with 369 additions and 2 deletions
|
|
@ -36,6 +36,20 @@ CATALOGOS = [
|
|||
"LISTAS", "CC0-1.0", True),
|
||||
("awesome-hacking", os.path.join("_pages", "index.rst"), "rst",
|
||||
"awesome-hacking (jekil)", "PENTESTERS", "", False),
|
||||
# Segunda tanda (16-17 ago). publico=False hasta verificar licencia: no salen
|
||||
# al indice publico, solo al personal que usa JARVIS en esta maquina.
|
||||
("awesome-sysadmin", "README.md", "md", "awesome-sysadmin", "SISTEMA", "", False),
|
||||
("awesome-cli-apps", "readme.md", "md", "awesome-cli-apps", "SISTEMA", "", False),
|
||||
("awesome-security", "README.md", "md", "awesome-security", "PENTESTERS", "", False),
|
||||
("awesome-pentest", "README.md", "md", "awesome-pentest", "PENTESTERS", "", False),
|
||||
("awesome-shell", "README.md", "md", "awesome-shell", "SISTEMA", "", False),
|
||||
("awesome-linux", "README.md", "md", "awesome-linux", "SISTEMA", "", False),
|
||||
# Tercera tanda (madrugada 17 ago). Todas publico=False hasta revisar licencia.
|
||||
("awesome-osint", "README.md", "md", "awesome-osint", "PENTESTERS", "", False),
|
||||
("awesome-devops", "README.md", "md", "awesome-devops", "SISTEMA", "", False),
|
||||
("awesome-docker", "README.md", "md", "awesome-docker", "SISTEMA", "", False),
|
||||
("awesome-go", "README.md", "md", "awesome-go", "SISTEMA", "", False),
|
||||
("awesome-python", "README.md", "md", "awesome-python", "SISTEMA", "", False),
|
||||
]
|
||||
|
||||
# markdown: - [Nombre](http...) - Descripcion ... `Lic` `Lenguaje`
|
||||
|
|
|
|||
61
nucleo/saber/busca_cli.py
Executable file
61
nucleo/saber/busca_cli.py
Executable file
|
|
@ -0,0 +1,61 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Busca en el indice del RAG desde la linea de comandos.
|
||||
|
||||
busca_cli.py "como saco una shell reversa"
|
||||
busca_cli.py --n 3 "gestor de contraseñas autoalojado"
|
||||
busca_cli.py --json "kerberoasting"
|
||||
|
||||
Es el mismo motor que usa el cerebro del naranja (saber.busca), expuesto para que
|
||||
lo llame cualquiera: un script, o un agente por su herramienta de terminal. Asi el
|
||||
carril verde (Hermes) puede consultar tus apuntes sin duplicar el RAG.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
AQUI = os.path.dirname(os.path.abspath(__file__))
|
||||
NUCLEO = os.path.dirname(AQUI)
|
||||
if NUCLEO not in sys.path:
|
||||
sys.path.insert(0, NUCLEO)
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description="Busca en los apuntes indexados (RAG)")
|
||||
p.add_argument("consulta", nargs="+", help="lo que quieres buscar")
|
||||
p.add_argument("--n", type=int, default=5, help="cuantos resultados (def. 5)")
|
||||
p.add_argument("--json", action="store_true", help="salida JSON en vez de texto")
|
||||
a = p.parse_args()
|
||||
|
||||
from saber import busca
|
||||
if not busca.disponible():
|
||||
print("no hay indice: corre nucleo/saber/indexa.py", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
consulta = " ".join(a.consulta)
|
||||
res = busca.busca(consulta, cuantos=a.n)
|
||||
|
||||
if a.json:
|
||||
salida = [{"herramienta": r.get("herramienta"),
|
||||
"fuente": r.get("fuente") or r.get("perfil"),
|
||||
"fichero": r.get("fichero"),
|
||||
"texto": r.get("texto", "")[:600]} for r in res]
|
||||
print(json.dumps(salida, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
if not res:
|
||||
print("sin resultados")
|
||||
return 0
|
||||
for i, r in enumerate(res, 1):
|
||||
fuente = r.get("fuente") or r.get("perfil") or ""
|
||||
cabecera = f"{i}. {r.get('herramienta', '?')}"
|
||||
if fuente:
|
||||
cabecera += f" ({fuente})"
|
||||
print(cabecera)
|
||||
print(" " + r.get("texto", "").strip().replace("\n", "\n ")[:500])
|
||||
print()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -43,3 +43,15 @@
|
|||
{"pregunta": "como escaneo redes wifi y capturo handshakes", "espera": ["airodump", "aircrack", "handshake", "wpa", "airgeddon"]}
|
||||
{"pregunta": "usar crackmapexec para barrer una red windows", "espera": ["crackmapexec", "cme ", "smb", "--shares", "spray"]}
|
||||
{"pregunta": "leer un hash ntlm y reutilizarlo por la red", "espera": ["ntlm", "hash", "relay", "responder", "ntlmrelayx"]}
|
||||
{"pregunta": "alternativa self-hosted a google analytics", "espera": ["analytic", "plausible", "matomo", "goatcounter"]}
|
||||
{"pregunta": "servidor multimedia para ver peliculas en casa", "espera": ["jellyfin", "plex", "streaming", "media"]}
|
||||
{"pregunta": "gestor de contraseñas autoalojado", "espera": ["password", "vaultwarden", "bitwarden", "contrasen"]}
|
||||
{"pregunta": "herramienta de linea de comandos para procesar json", "espera": ["jq", "jp", "fx", "json"]}
|
||||
{"pregunta": "monitorizar los recursos del sistema en linux", "espera": ["htop", "netdata", "monitor", "glances"]}
|
||||
{"pregunta": "framework de plugins para la shell zsh", "espera": ["zsh", "oh-my-zsh", "prezto", "shell"]}
|
||||
{"pregunta": "agente de inteligencia artificial para pentesting automatico", "espera": ["strix", "pentest", "vulnerab"]}
|
||||
{"pregunta": "programa para modelado 3d y renderizado", "espera": ["blender", "3d"]}
|
||||
{"pregunta": "software para grabar y mezclar audio multipista", "espera": ["ardour", "audio"]}
|
||||
{"pregunta": "herramienta para automatizar el hogar domotica", "espera": ["home", "iot", "domot", "assistant"]}
|
||||
{"pregunta": "distribucion de linux para privacidad", "espera": ["tails", "privac", "linux"]}
|
||||
{"pregunta": "escaner de vulnerabilidades web", "espera": ["nuclei", "nikto", "sqlmap", "scanner", "vulnerab"]}
|
||||
|
|
|
|||
71
nucleo/saber/enriquece/reentrena_noche.sh
Executable file
71
nucleo/saber/enriquece/reentrena_noche.sh
Executable file
|
|
@ -0,0 +1,71 @@
|
|||
#!/usr/bin/env bash
|
||||
# Reentrenamiento nocturno AMPLIADO del RAG. Encadena las fases y las deja
|
||||
# corriendo horas, sin tocar gitea. Cada fase es reanudable por su cuenta.
|
||||
#
|
||||
# ./reentrena_noche.sh
|
||||
#
|
||||
# Fases:
|
||||
# 1. Espera a que termine cualquier reindex en curso.
|
||||
# 2. Sintesis FRESCA de todo el contenido nuevo (conocimiento + catalogo +
|
||||
# apps instaladas + docs de strix) -> preguntas-gancho.
|
||||
# 3. Reindex personal con los ganchos horneados.
|
||||
# 4. Evaluacion hit@k (numeros).
|
||||
# 5. Refresco de COFRE: cosecha + sintesis de los apuntes.
|
||||
# 6. Reindex final + evaluacion.
|
||||
#
|
||||
# Todo el ruido va al log; a la salida estandar solo hitos con hora.
|
||||
set -uo pipefail
|
||||
|
||||
AQUI=$(cd -P "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)
|
||||
NUCLEO=$(cd "$AQUI/../.." && pwd)
|
||||
PY="$NUCLEO/venv/bin/python"
|
||||
[ -x "$PY" ] || PY=python3
|
||||
DATOS="$NUCLEO/datos"
|
||||
LOG="${1:-$DATOS/reentrena_noche.log}"
|
||||
|
||||
hora() { date +%H:%M:%S; }
|
||||
hito() { echo "[$(hora)] $*" | tee -a "$LOG"; }
|
||||
|
||||
cd "$NUCLEO"
|
||||
hito "=== ARRANCA reentrenamiento nocturno ampliado ==="
|
||||
|
||||
# --- Fase 1: no competir con un reindex en curso ---
|
||||
while pgrep -f "saber.indexa" | grep -qv $$ 2>/dev/null; do
|
||||
sleep 20
|
||||
done
|
||||
hito "fase 1 ok: no hay reindex compitiendo"
|
||||
|
||||
# --- Fase 2: sintesis fresca del contenido nuevo ---
|
||||
hito "fase 2: sintesis del catalogo+nuevo (fresca, puede tardar 1-2 h)"
|
||||
rm -f "$DATOS/sintesis-catalogo.jsonl" "$DATOS/sintesis-catalogo.jsonl.hecho"
|
||||
"$PY" -m saber.enriquece.sintetiza_catalogo >>"$LOG" 2>&1
|
||||
hito "fase 2 ok: $(wc -l < "$DATOS/sintesis-catalogo.jsonl" 2>/dev/null) preguntas-gancho nuevas"
|
||||
|
||||
# --- Fase 3: reindex con los ganchos ---
|
||||
hito "fase 3: reindex personal con ganchos"
|
||||
"$PY" -m saber.indexa >>"$LOG" 2>&1
|
||||
hito "fase 3 ok: indice reconstruido"
|
||||
|
||||
# --- Fase 4: evaluacion ---
|
||||
hito "fase 4: evaluacion hit@k"
|
||||
"$PY" -m saber.enriquece.evalua "$DATOS/saber.jsonl" "$DATOS/saber.jsonl" 2>>"$LOG" \
|
||||
| grep -iE "hit@|sim" | head -3 | tee -a "$LOG"
|
||||
|
||||
# --- Fase 5: refresco de COFRE (cosecha + sintesis) ---
|
||||
if [ "${SIN_COFRE:-0}" = "1" ]; then
|
||||
hito "fase 5: OMITIDA (SIN_COFRE=1, el refresco de COFRE ya se hizo)"
|
||||
else
|
||||
hito "fase 5: refresco de COFRE (cosecha + sintesis, largo)"
|
||||
"$PY" -m saber.enriquece.cosecha >>"$LOG" 2>&1 && hito " cosecha ok" || hito " cosecha fallo (sigo)"
|
||||
rm -f "$DATOS/sintesis.jsonl.hecho"
|
||||
"$PY" -m saber.enriquece.sintetiza >>"$LOG" 2>&1 && hito " sintesis COFRE ok" || hito " sintesis COFRE fallo (sigo)"
|
||||
fi
|
||||
|
||||
# --- Fase 6: reindex final + evaluacion ---
|
||||
hito "fase 6: reindex final"
|
||||
"$PY" -m saber.indexa >>"$LOG" 2>&1
|
||||
hito "fase 6: evaluacion final"
|
||||
"$PY" -m saber.enriquece.evalua "$DATOS/saber.jsonl" "$DATOS/saber.jsonl" 2>>"$LOG" \
|
||||
| grep -iE "hit@|sim" | head -3 | tee -a "$LOG"
|
||||
|
||||
hito "=== FIN del reentrenamiento nocturno ==="
|
||||
|
|
@ -61,6 +61,14 @@ def _fragmentos():
|
|||
frags.append(e)
|
||||
for e in awesome.entradas(): # catalogo: ya trae `publico`
|
||||
frags.append(e)
|
||||
for e in indexa._instalado(): # apps de esta maquina: personal
|
||||
e = dict(e)
|
||||
e["publico"] = False
|
||||
frags.append(e)
|
||||
for e in indexa._docs_extra(): # docs de strix, etc.: personal
|
||||
e = dict(e)
|
||||
e["publico"] = False
|
||||
frags.append(e)
|
||||
frags.sort(key=lambda d: (0 if d.get("publico") else 1,
|
||||
0 if d.get("tipo") == "metodologia" else 1))
|
||||
return frags
|
||||
|
|
|
|||
|
|
@ -200,6 +200,57 @@ def _ganchos(solo_publico=False):
|
|||
return ganchos
|
||||
|
||||
|
||||
def _instalado():
|
||||
"""Las aplicaciones instaladas en esta maquina (instalado.py). Personal."""
|
||||
try:
|
||||
try:
|
||||
from saber import instalado
|
||||
except ImportError:
|
||||
import instalado
|
||||
except ImportError:
|
||||
return []
|
||||
fragmentos = []
|
||||
for e in instalado.entradas():
|
||||
e.pop("publico", None)
|
||||
fragmentos.append(e)
|
||||
return fragmentos
|
||||
|
||||
|
||||
# Repos de ~/COFRE/CODERS cuyos docs (markdown) vale la pena indexar aunque no
|
||||
# sean un perfil de COFRE. Personal: son herramientas de terceros clonadas aqui.
|
||||
DOCS_REPOS = [
|
||||
("strix", "PENTESTERS", ["README.md", "AGENTS.md", "docs", "benchmarks"]),
|
||||
]
|
||||
|
||||
|
||||
def _docs_extra():
|
||||
"""Docs markdown de herramientas clonadas en CODERS (strix, etc.). Personal."""
|
||||
coders = os.path.join(COFRE, "CODERS")
|
||||
fragmentos = []
|
||||
for nombre, perfil, rutas in DOCS_REPOS:
|
||||
base = os.path.join(coders, nombre)
|
||||
if not os.path.isdir(base):
|
||||
continue
|
||||
ficheros = []
|
||||
for r in rutas:
|
||||
p = os.path.join(base, r)
|
||||
if os.path.isfile(p) and p.endswith(".md"):
|
||||
ficheros.append(p)
|
||||
elif os.path.isdir(p):
|
||||
for raiz, dirs, fs in os.walk(p):
|
||||
if IGNORA.search(raiz):
|
||||
dirs[:] = []
|
||||
continue
|
||||
ficheros += [os.path.join(raiz, f) for f in fs if f.endswith(".md")]
|
||||
for doc in ficheros:
|
||||
for trozo in trocea(doc):
|
||||
fragmentos.append({
|
||||
"tipo": "metodologia", "herramienta": nombre, "perfil": perfil,
|
||||
"tema": nombre, "fichero": os.path.relpath(doc, coders),
|
||||
"texto": trozo})
|
||||
return fragmentos
|
||||
|
||||
|
||||
def _conocimiento():
|
||||
"""El pack de metodologia y manuales que viene con el repo (conocimiento/)."""
|
||||
fragmentos = []
|
||||
|
|
@ -325,14 +376,24 @@ def construye(solo_cuenta=False, solo_cofre=False):
|
|||
if catalogo:
|
||||
print(f" CATALOGO {len(catalogo):5d} fragmentos (awesome lists)")
|
||||
|
||||
instalado = _instalado()
|
||||
if instalado:
|
||||
print(f" INSTALADO {len(instalado):5d} apps/paquetes de esta maquina")
|
||||
|
||||
docs_extra = _docs_extra()
|
||||
if docs_extra:
|
||||
print(f" DOCS-CODERS {len(docs_extra):5d} fragmentos (strix, etc.)")
|
||||
|
||||
ganchos = _ganchos(solo_publico=False)
|
||||
if ganchos:
|
||||
print(f" GANCHOS {len(ganchos):5d} preguntas sintetizadas")
|
||||
|
||||
todo = fichas + fragmentos + sistema + conocimiento + catalogo + ganchos
|
||||
todo = (fichas + fragmentos + sistema + conocimiento + catalogo
|
||||
+ instalado + docs_extra + ganchos)
|
||||
print(f"\n total: {len(fichas)} fichas + {len(fragmentos)} apuntes + "
|
||||
f"{len(sistema)} sistema + {len(conocimiento)} conocimiento + "
|
||||
f"{len(catalogo)} catalogo + {len(ganchos)} ganchos = {len(todo)} entradas")
|
||||
f"{len(catalogo)} catalogo + {len(instalado)} instalado + "
|
||||
f"{len(docs_extra)} docs + {len(ganchos)} ganchos = {len(todo)} entradas")
|
||||
if solo_cuenta:
|
||||
return 0
|
||||
|
||||
|
|
|
|||
140
nucleo/saber/instalado.py
Executable file
140
nucleo/saber/instalado.py
Executable file
|
|
@ -0,0 +1,140 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Indexa las aplicaciones instaladas en ESTA maquina: que hay y para que sirve.
|
||||
|
||||
./instalado.py di cuantas entradas saldrian
|
||||
./instalado.py --volcado imprime unas cuantas de muestra
|
||||
|
||||
Dos fuentes, ambas locales:
|
||||
1. Los .desktop (/usr/share/applications, ~/.local, flatpak): las apps que el
|
||||
usuario ve y lanza, con su nombre, para que sirven (Comment) y el comando.
|
||||
2. Los paquetes que el usuario instalo a mano (apt-mark showmanual), con su
|
||||
descripcion de una linea. No las dependencias: eso es ruido.
|
||||
|
||||
Es privado por definicion —dice que software tiene esta maquina—, asi que
|
||||
indexa.py lo mete SOLO en el indice personal, nunca en el publico ni en gitea.
|
||||
"""
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
DIRS_DESKTOP = [
|
||||
"/usr/share/applications",
|
||||
os.path.expanduser("~/.local/share/applications"),
|
||||
"/var/lib/flatpak/exports/share/applications",
|
||||
os.path.expanduser("~/.local/share/flatpak/exports/share/applications"),
|
||||
"/var/lib/snapd/desktop/applications",
|
||||
]
|
||||
|
||||
|
||||
def _campo(texto, clave):
|
||||
m = re.search(r"^%s=(.+)$" % re.escape(clave), texto, re.M)
|
||||
return m.group(1).strip() if m else ""
|
||||
|
||||
|
||||
def _apps_desktop():
|
||||
vistos = set()
|
||||
for d in DIRS_DESKTOP:
|
||||
for ruta in sorted(glob.glob(os.path.join(d, "*.desktop"))):
|
||||
base = os.path.basename(ruta)
|
||||
if base in vistos:
|
||||
continue
|
||||
vistos.add(base)
|
||||
try:
|
||||
with open(ruta, encoding="utf-8", errors="ignore") as f:
|
||||
txt = f.read()
|
||||
except OSError:
|
||||
continue
|
||||
if _campo(txt, "NoDisplay").lower() == "true":
|
||||
continue
|
||||
nombre = _campo(txt, "Name")
|
||||
if not nombre:
|
||||
continue
|
||||
comentario = _campo(txt, "Comment") or _campo(txt, "GenericName")
|
||||
categorias = _campo(txt, "Categories").replace(";", ", ").strip(", ")
|
||||
exe = re.sub(r"%[a-zA-Z]", "", _campo(txt, "Exec")).strip()
|
||||
cmd = exe.split()[0] if exe else ""
|
||||
partes = [f"{nombre}"]
|
||||
if comentario:
|
||||
partes.append(f"— {comentario}")
|
||||
if cmd:
|
||||
partes.append(f"(se lanza con: {cmd})")
|
||||
if categorias:
|
||||
partes.append(f"[{categorias}]")
|
||||
yield {
|
||||
"tipo": "instalado", "herramienta": nombre, "perfil": "SISTEMA",
|
||||
"publico": False, "tema": categorias.lower(),
|
||||
"fichero": f"app instalada: {base}",
|
||||
"texto": "Aplicacion instalada: " + " ".join(partes),
|
||||
}
|
||||
|
||||
|
||||
def _paquetes_manuales():
|
||||
"""Los paquetes apt que instalo el usuario (no dependencias), con su resumen."""
|
||||
try:
|
||||
man = subprocess.run(["apt-mark", "showmanual"], capture_output=True,
|
||||
text=True, timeout=30).stdout.split()
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return
|
||||
if not man:
|
||||
return
|
||||
# resumen de cada uno en un solo dpkg-query (rapido)
|
||||
try:
|
||||
salida = subprocess.run(
|
||||
["dpkg-query", "-W", "-f=${Package}\t${binary:Summary}\n", *man],
|
||||
capture_output=True, text=True, timeout=60).stdout
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return
|
||||
lineas = []
|
||||
for l in salida.splitlines():
|
||||
if "\t" not in l:
|
||||
continue
|
||||
pkg, resumen = l.split("\t", 1)
|
||||
if resumen.strip():
|
||||
lineas.append(f"{pkg}: {resumen.strip()}")
|
||||
# se agrupan en fragmentos (no una entrada por paquete: son cientos)
|
||||
buffer = "Paquetes que el usuario instalo a mano en esta maquina:\n"
|
||||
for ln in lineas:
|
||||
if len(buffer) + len(ln) > 1100 and buffer:
|
||||
yield _pkg_entry(buffer)
|
||||
buffer = "Paquetes instalados (cont.):\n"
|
||||
buffer += "- " + ln + "\n"
|
||||
if buffer.strip():
|
||||
yield _pkg_entry(buffer)
|
||||
|
||||
|
||||
def _pkg_entry(texto):
|
||||
return {"tipo": "instalado", "herramienta": "paquetes apt", "perfil": "SISTEMA",
|
||||
"publico": False, "tema": "paquetes", "fichero": "apt-mark showmanual",
|
||||
"texto": texto.strip()}
|
||||
|
||||
|
||||
def entradas():
|
||||
yield from _apps_desktop()
|
||||
yield from _paquetes_manuales()
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--volcado", action="store_true")
|
||||
a = p.parse_args()
|
||||
apps = pkgs = 0
|
||||
muestras = []
|
||||
for e in entradas():
|
||||
if e["herramienta"] == "paquetes apt":
|
||||
pkgs += 1
|
||||
else:
|
||||
apps += 1
|
||||
if len(muestras) < 6:
|
||||
muestras.append(e)
|
||||
print(f" {apps} apps (.desktop) + {pkgs} fragmentos de paquetes apt")
|
||||
if a.volcado:
|
||||
for e in muestras:
|
||||
print(" -", e["texto"][:160].replace("\n", " "))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue