# Enriquecer el RAG a nivel de pentesting profesional El RAG del naranja ya funcionaba, pero se dejaba fuera lo mejor que hay en el disco. Medido antes de empezar: **de las 72 notas del `OSCP_Vault` —la metodologia OSCP escrita a mano, con comandos reales— había 0 en el índice.** `indexa.py` las descartaba por dos motivos a la vez: el nombre (`SQL injection.md` no lleva "notas" ni "guia") y la profundidad (viven a 5 niveles). Este pipeline las rescata, multiplica su recuperabilidad con preguntas generadas, y **mide** que el resultado es mejor antes de tocar nada. ## Correrlo ```bash ./correr_noche.sh # las cuatro fases, y decide si promociona ./correr_noche.sh --sin-promo # igual, pero deja el vivo intacto ``` Es reanudable: cada fase se salta si ya está hecha, así que si se corta, relanzarlo continúa. La fase larga (síntesis) es reanudable fragmento a fragmento. ## Las cuatro fases | | Script | Qué hace | Coste | |---|---|---|---| | 1 | `cosecha.py` | rescata la metodología de COFRE que faltaba | segundos, CPU | | 2 | `sintetiza.py` | preguntas en castellano por cada fragmento | **horas**, modelo local | | 3 | `reindexa.py` | une todo y calcula vectores → `saber.jsonl.nuevo` | minutos, CPU | | 4 | `evalua.py` | mide recuperación vivo vs candidato | segundos | ### 1 · Cosecha Donde `indexa.py` es prudente (solo ficheros que *parecen* documentación, 3 niveles), esto es agresivo con el oro: las carpetas de metodología (OSCP vaults, apuntes, cheatsheets) enteras, sin límite de profundidad. Fuera de ahí pone un **tope por herramienta**: si un repo tiene más de 25 documentos, es una base de datos (exploitdb son 29.925 ficheros), no unos apuntes, y solo se coge su README. Así el oro no se ahoga en ruido. ### 2 · Síntesis — indexación multi-representación El punto flaco de los embeddings estáticos: "cómo saco una shell reversa" no se parece vectorialmente a `bash -i >& /dev/tcp/...`. La solución es **embeber la pregunta y devolver el fragmento**. Por cada fragmento, el modelo local escribe las preguntas que responde; se indexa la pregunta, pero lo que se le muestra al cerebro es el fragmento **literal**. Clave: el modelo **solo escribe preguntas, nunca reescribe comandos**. Si inventa una pregunta rara, esa entrada recupera peor y ya está; no corrompe una respuesta. Y el fragmento crudo sigue en el índice por su lado. ### 3 · Reindexado Une el índice vivo (comandos Linux, glosario, fichas), la cosecha y la síntesis; deduplica por prefijo; calcula los vectores con el mismo `model2vec` de siempre. Escribe a `saber.jsonl.nuevo`. **No toca el vivo.** `busca.py` no cambia. ### 4 · Evaluación `eval_set.jsonl` son ~45 preguntas de pentesting con las palabras que un fragmento correcto debe contener. Mide `hit@1`, `hit@5` y similitud media, del índice vivo contra el candidato. Determinista y reproducible, sin juez-LLM. ## La decisión, y por qué es segura El índice vivo **solo se sustituye si la evaluación dice que el candidato es mejor**, y antes se guarda `saber.jsonl.antes-`. Si empeora, se queda el vivo y el candidato espera revisión. Siempre reversible con un `cp`. ## Ficheros que genera (en `../../datos/`) cosecha.jsonl la metodología rescatada, sin vectores sintesis.jsonl las preguntas generadas (+ .hecho, el marcador) saber.jsonl.nuevo el índice candidato, con vectores informe_rag.txt los números de la evaluación noche_rag.log el registro de la noche saber.jsonl.antes-* copia del índice anterior, si se promocionó ## Iteraciones posteriores a la primera noche - **Troceado que no tira comandos cortos** (`cosecha.py`). El troceado por encabezado descartaba toda sección de menos de 60 caracteres, y eso perdía el oro: `## Detección time-based` + `' AND SLEEP(5)-- -` son 50. Ahora una sección corta se pega a la siguiente y cada fragmento lleva delante el título de la nota. `SLEEP` pasó de 0 a 13 apariciones en el índice; hit@1 80→82 %. El salto en la métrica es pequeño porque las preguntas que quedan son cross-lingual, y de eso no salva el troceado. - **`experimento_embedder.py`** — mide, sin tocar nada, si un transformer (e5, bge) supera al model2vec estático. **Resultado (16 ago): NO compensa.** `intfloat/multilingual-e5-base` empató con el estático (hit@1 86 %, hit@5 95 % los dos): arregla las 2 preguntas que el estático fallaba, pero rompe otras 2 distintas. A cambio pediría ~50-150 ms por consulta en CPU y una dependencia de torch en cada búsqueda. Medido antes de cambiar → **el estático se queda.** El trabajo de recuperación lo hace la búsqueda híbrida (coseno + bono por palabras), no el embedder, y por eso subir el embedder no mueve la aguja. Reproducible: `./experimento_embedder.py`. - **Re-síntesis limpia** sobre el troceado nuevo (`SABER_BASE` en `reindexa.py` reconstruye desde el índice original en vez de apilar, para no inflar). Da a los comandos rescatados (SLEEP y demás) su gancho-pregunta en castellano, que es lo que de verdad ataca las consultas cross-lingual —no el embedder—. ## Nota sobre la tarjeta La síntesis usa el modelo local por `127.0.0.1:11434`. Fija `qwen3.5:4b-jarvis` (el naranja, que no inventa comandos). Si dejas el TUI verde abierto, ollama tiene que cambiar de modelo en cada petición y la noche va más lenta: para la síntesis más rápida, cierra el verde.