Skip to main content
Glama

pentest-recorder · pentest-mcp

Tu terminal es el mejor registro de un compromiso que jamás tendrás. Esto lo convierte en uno.

CI Python MCP local-first licence: MIT

Dos horas después de empezar una evaluación, tienes una credencial de dominio, cuatro hosts, un recurso compartido que puedes leer y ninguna idea de en qué panel viste todo eso. Así que te desplazas. O vuelves a ejecutar la enumeración. O lo pegas todo en un archivo de notas a mano, y luego lo haces de nuevo mañana.

pentest-recorder observa tu sesión tmux existente, conserva los bytes exactos que produjo tu terminal y los convierte en estado estructurado de compromiso. pentest-mcp entrega ese estado a cualquier agente compatible con MCP, para que pueda responder ¿dónde obtuve la contraseña de svc_backup? con el segmento, el panel, la marca de tiempo y los bytes originales.

Conservas tu VM, tu tmux, tus VPN y pivotes, tus alias, tus listas de palabras. Nada se envuelve. Nada se reemplaza.

  tmux panes
      │
      ▼
  raw bytes ──────────────────────────────►  raw/pane-000003.log
      │                                       authoritative · never parsed
      │ terminal emulation
      ▼
  segments  ◄── byte-addressable, immutable
      │
      │ LLM (local by default)
      ▼
  observations  ◄── append-only, every fact cites its source
      │
      │ deterministic rebuild
      ▼
  entities · relationships · auth log
      │
      ├──────────────► Obsidian vault   (a projection, not the store)
      │
      └──────────────► pentest-mcp ────► Claude Code · Codex · any MCP agent

Esto no es un agente de pentesting autónomo. La capa de datos contiene la verdad y la memoria; el agente hace el razonamiento. El grabador nunca se conecta a nada que hayas escaneado — garantizado por una prueba, no por buenas intenciones.


Instalación

git clone git@github.com:lucianoengel/pentest-mcp.git
cd pentest-mcp
./install.sh

Python 3.11+, tmux y SQLite con FTS5 — esto último lo comprueba el instalador, porque algunos Pythons de las distribuciones lo omiten. Sin Docker, sin servidor de base de datos, sin interfaz de navegador. Tres paquetes de terceros en total: pyte, httpx, mcp.

Actualización

cd pentest-mcp
git pull
./install.sh

Volver a ejecutar el instalador es la actualización. Reconstruye desde el checkout, conserva tu config.toml existente y no toca los datos de los compromisos. Si un compromiso fue escrito por una versión anterior, su almacén se migra la próxima vez que un comando lo abra para escritura — pentest-recorder status informa la versión del esquema y lo dice si no coinciden.

Detén primero el grabador si hay uno en ejecución (pentest-recorder stop), ya que un demonio en ejecución sigue usando el código con el que se inició.

Related MCP server: mcp-ssh-interactive

Uso

pentest-recorder init inlanefreight --client "ACME" --scope "172.16.119.0/24"
tmux new -s inlanefreight
pentest-recorder start

Luego trabaja con normalidad. Cada panel de esa sesión se captura, incluidos los paneles y ventanas que abras más tarde.

pentest-recorder status                  # what is — and is NOT — being captured
pentest-recorder search 'Summer2026!'    # find the exact string, across everything
pentest-recorder pause                   # stop capturing, right now
pentest-recorder sync                    # extract, rebuild, export

Conectar un agente

claude mcp add pentest -- ~/.local/bin/pentest-mcp --engagement inlanefreight

Ahora pregúntale cosas.

¿qué credenciales tengo que aún no he validado?

// list_entities(type="credential", filter={"validated": false})
{
  "items": [{
    "id": "credential:INLANEFREIGHT/svc_backup:password:HolyMoly123!",
    "type": "credential",
    "data": {
      "username": "svc_backup",
      "domain": "INLANEFREIGHT",
      "secret": "HolyMoly123!",      // exact, never normalized
      "secret_type": "password",
      "status": "unvalidated"
    },
    "fact_type": "CONFIRMED",
    "observation_count": 1
  }],
  "total": 1
}

¿de dónde salió esa contraseña?

// get_provenance(entity_id="credential:INLANEFREIGHT/svc_backup:...")
{
  "observations": [{
    "kind": "credential",
    "source": "extraction",
    "actor": "ollama/qwen2.5-coder:7b",
    "verified": true,                 // appeared verbatim in the source
    "segment_ids": [1]
  }],
  "segments": [{
    "id": 1,
    "terminal": "inlanefreight:2.1",
    "ts_start": "2026-08-26T14:32:11+00:00",
    "command": "cat /mnt/backup/scripts/backup.ini",
    "cwd": "/home/kali/eng",
    "raw_path": ".../raw/pane-000003.log",
    "byte_start": 0,
    "byte_end": 125                   // the original bytes, still on disk
  }]
}

ponme al día

// get_engagement_summary()
hosts: 2 · services: 1 · identities: 1
credentials: 2  (1 unvalidated)
auth: 1 successful, 1 failed
findings: 1 candidate · open tasks: 0
hosts_with_no_service_recorded: 1     ← DC01 is under-enumerated
entities_with_unverified_fields: 0
segments: 1  (1 pending extraction)

Ese resumen es el índice del agente. Es la razón por la que no existe una herramienta list_unvalidated_credentials() — los recuentos le dicen al agente qué preguntas merecen la pena, así que doce herramientas de lectura cubren lo que de otro modo necesitaría treinta.


Lo que captura — y lo que deliberadamente no captura

Ve dentro de sesiones anidadas. Una reverse shell capturada, ssh, evil-winrm, msfconsole, sqlplus. Ahí es donde vive gran parte de la buena evidencia, y es exactamente lo que las herramientas de historial de shell no pueden alcanzar:

$ nc -lvnp 4444                    ← the only local command that ever runs
connect to [10.10.14.7] from (UNKNOWN) [172.16.119.30] 51422
C:\inetpub\wwwroot> type web.config
  <add name="prod" connectionString="...;Password=P@ssw0rd#2026;" />
                                   ↑ captured, extracted, attributed to nothing

El comando que lo produjo se registra solo cuando se conoce de verdad. Dentro de esa reverse shell, command es nc -lvnp 4444 — con sinceridad — y exit_code permanece vacío en lugar de adivinarse.

Se detiene cuando se lo dices:

cómo

la ventana del agente

excluida por defecto — la salida del propio agente nunca debe volver a entrar como evidencia

un panel

tmux set -p @pentest-record off

una ventana

tmux set -w @pentest-record off

todo, inmediatamente

pentest-recorder pause

un panel que ya canalizas

detectado, se deja solo, se informa

status enumera cada panel en alcance que no se está capturando, con el motivo. Un panel no monitoreado en silencio es el peor fallo que esta herramienta puede tener, así que nunca es una nota al pie:

Engagement:  inlanefreight  (client: ACME)
Recorder:    running (pid 48213) since 2026-08-26T13:58:02+00:00
tmux server: 347338
Capturing:   3 pane(s)
     %1  inlanefreight:1:recon.0  (0 B buffered)
     %4  inlanefreight:2:ad.0     (2145 B buffered)
     %7  inlanefreight:3:shell.0  (0 B buffered)

NOT capturing 2 pane(s) in scope:
     %9  inlanefreight:5:agent.0  -- window excluded by configuration
    %11  inlanefreight:4:web.0    -- already piped by another tool; left untouched

Segments:    184 total, 3 pending, 181 extracted, 0 failed
Extraction:  ollama / qwen2.5-coder:7b
  Local provider: no engagement data leaves this machine.

Dónde viven tus datos

~/.local/share/pentest-recorder/engagements/inlanefreight/
├── engagement.db     one ordinary SQLite file — the canonical store
├── raw/              exact terminal bytes, rotated and gzipped
└── evidence/

Directorios 0700, archivos 0600. Los registros sin procesar son lo más sensible de la máquina — credenciales de dominio en texto plano y datos de clientes están en ellos. Trata ese directorio como botín.

Todo es inspeccionable con herramientas ordinarias. Una copia de seguridad es cp -r; una auditoría es una consulta SQL:

sqlite3 engagement.db \
  "SELECT json_extract(data,'\$.username'), json_extract(data,'\$.status')
   FROM entities WHERE type='credential';"

Los compromisos separados están separados en disco

Un directorio y un archivo de base de datos por compromiso — no una tabla compartida con un filtro. Dos clientes que usan el mismo espacio RFC1918 siguen siendo dos conjuntos distintos de entidades, y buscar la contraseña de uno en el otro no devuelve nada.

pentest-recorder init acme   --client "ACME"   --local-only
pentest-recorder init globex --client "Globex" --local-only

Cada uno se vincula a la sesión tmux del mismo nombre. Si dos grabadores apuntan alguna vez a un mismo panel, pipe-pane -o se niega a desplazar al primero y el segundo informa del panel como no monitoreado.

Al final de un compromiso:

pentest-recorder purge -e acme --include-vault

Lo que llega al modelo

Tres cosas se interponen entre la captura y un modelo, y cada una se desactiva de forma independiente en [extraction].

Los hechos que el texto determina se derivan sin un modelo. Una dirección, una URL, una ruta UNC, password=X, y credenciales en formatos reconocidos — NTLMv2, tickets Kerberos, filas pwdump, JWTs, claves PEM — se leen directamente del texto. Cuando el formato lleva la cuenta, como la mayoría, el propietario proviene del propio valor:

svc_qualys::INLANEFREIGHT:1122334455667788:AB12…:0101…
└────┬────┘  └─────┬─────┘
  username      domain        ← both are part of the value

Medido contra un corpus de nueve formatos: el paso de reglas recupera 9/9 byte-idénticos con 9/9 atribuciones correctas; qwen2.5-coder:7b al que se le pide transcribir los mismos valores logra 6/9 y 5/9. Lo que no hará es reconocer un valor que solo significa algo por dónde lo imprime una herramienta — microsoft-ds es un servicio porque nmap tiene una columna SERVICE, y eso sigue siendo trabajo del modelo.

Los tokens largos se reemplazan antes de que el modelo los vea. Lo que queda es un marcador de posición, su longitud y clase de carácter, y todo el contexto circundante:

[SMB] NTLMv2-SSP Hash : svc_qualys::INLANEFREIGHT:1122334455667788:<Ta1b2c3d4:1>
                        └──────────── kept, so the model can attribute it ────┘

El grabador sustituye los bytes reales de nuevo. El error de transcripción deja de ser algo que se detecta y se convierte en algo que no puede ocurrir. Mantener los identificadores visibles importa: ocultar toda la credencial baja la clasificación al 44%, mantenerlos la sube a 78% — mejor que mostrar al modelo el valor sin procesar.

La repetición se colapsa y los segmentos vacíos se omiten. Las líneas idénticas se convierten en una instancia y un recuento, con cada valor distinto preservado. Un segmento que no lleva ningún candidato ni ningún valor que el compromiso no conozca ya nunca se envía — la decisión se basa en valores desconocidos, no en formas de línea, porque a mitad de compromiso cada forma es familiar y connected to \\SQL01\payroll se descartaría junto con un nuevo host y recurso compartido. Los segmentos omitidos registran el motivo y siguen siendo reprocesables.

Los números detrás de todo esto están en bench/BASELINE.md.

Cuando los datos salen de la máquina

La extracción necesita un modelo de lenguaje, y no hay opción de redacción — para extraer un secreto tienes que enviar el secreto.

Así que el proveedor por defecto es local (Ollama en 127.0.0.1), y nada sale de tu máquina a menos que lo cambies. Si configuras un proveedor remoto, start te dice exactamente qué se transmitiría y se niega hasta que lo reconozcas:

Extraction is configured to use openai (gpt-4o-mini) at
  https://api.openai.com/v1

This sends captured terminal output to that service. In a penetration
test that includes, in full and unredacted:
  - plaintext passwords, password hashes, tokens and API keys
  - usernames, domains, internal hostnames and IP addresses
  - file contents, share names and command output
  - vulnerability evidence and client-identifying data

There is no redaction option: extracting a secret requires sending it.

Para trabajo de clientes donde eso está contractualmente prohibido:

pentest-recorder init acme --local-only

Ese compromiso rechaza un proveedor remoto permanentemente, independientemente de lo que diga la configuración después — e informa del rechazo, mientras la captura continúa.

Las claves API se leen del entorno, nombradas por api_key_env en la configuración. La clave nunca se escribe en el archivo de configuración, los datos del compromiso, los registros o el Markdown exportado.


Obsidian

Apunta obsidian.vault_path a cualquier lugar y el compromiso se proyecta en Markdown ordinario — panel de control, hosts, credenciales, hallazgos, línea de tiempo y una nota por host, con enlaces cruzados mediante wikilinks. Sin plugin, y Obsidian en sí es opcional — son archivos de texto; cat, grep y git funcionan bien.

# Credentials

| Identity | Secret | Type | Validated on | Source |
|---|---|---|---|---|
| INLANEFREIGHT\fiona     | `Summer2026!`  | Password | SMB FILE01 | Segment 1 |
| INLANEFREIGHT\svc_backup| `HolyMoly123!` | Password | Not yet    | Segment 1 |

La bóveda es una proyección, no el almacén: bórrala, vuelve a exportar, no pierdes nada. Los archivos generados llevan el front matter generated_by: pentest-recorder, y el exportador nunca sobrescribe un archivo que carezca de ese marcador — escribe al lado y te lo dice. Tus propias notas viven en Notes/, que nunca se toca.

include_secrets es include por defecto, porque el seguimiento preciso de credenciales es el punto central. Establece redact o partial si la bóveda se sincroniza a algún lugar donde preferirías que no; eso afecta solo a la proyección.


Configuración

~/.config/pentest-recorder/config.toml — genéralo con pentest-recorder config --write. Cada ajuste tiene un valor por defecto documentado, y un valor inválido se informa por nombre sin aplicar nada.

[data]
root = "~/.local/share/pentest-recorder"

[obsidian]
vault_path = "~/Obsidian/Pentests"
include_secrets = "include"        # include | redact | partial

[tmux]
session = "@engagement"            # or a glob such as "client-*"
exclude_windows = ["agent"]

[capture]
idle_flush_seconds = 3.0
max_segment_bytes = 65536
rotate_bytes = 134217728

[extraction]
rules = true                       # derive facts the text determines
redact = true                      # replace long tokens before prompting
collapse = true                    # collapse repeated lines
route = true                       # skip segments carrying nothing new
escalation_model = ""              # optional stronger model for hard cases

[llm]
provider = "ollama"                # ollama | openai | openai-compatible
model = "qwen2.5-coder:7b"
base_url = "http://127.0.0.1:11434"
api_key_env = "OPENAI_API_KEY"     # names the variable, never holds the key

[mcp]
default_engagement = ""

Integración opcional con el shell

Añade el comando exacto, el directorio de trabajo y el código de salida a los segmentos desde tu shell externo:

source /path/to/pentest-mcp/shell/pentest-recorder.sh

La captura funciona completamente sin ello, y la actividad dentro de una sesión anidada permanece correctamente sin atribuir en lugar de adivinarse.


Cómo se mantiene unido

Cuatro clases de almacenamiento, cada una con exactamente una regla:

clase

tablas

regla

inmutable

segments, pane_instances

solo añadir; nunca reescribir

solo añadir

observations

superado, nunca editado

derivada

entities, relationships, auth_attempts

se descarta y reconstruye desde observaciones

con estado

findings, tasks, notes

tuyas; la derivación nunca las toca

Cinco consecuencias que vale la pena conocer:

Un fallo del modelo no cuesta nada. tmux escribe la salida del panel en un archivo plano; el grabador lo sigue desde un desplazamiento de bytes con punto de control. Nada en la ruta de captura depende de que este proceso esté en ejecución. El grabador muere → los bytes siguen cayendo en el disco y se recogen al reiniciar. Proveedor caído → los segmentos permanecen pendientes.

Cada literal extraído se comprueba contra su fuente. Un valor que no aparece textualmente se conserva pero se marca, y se muestra como unverified a través de MCP. HolyMoly123! convirtiéndose silenciosamente en HolyMoly123 es exactamente el fallo que esta herramienta existe para prevenir — y la coincidencia de subcadenas simple no puede detectarlo, ya que la truncación es una subcadena de la verdad.

La fusión es emergente. Las entidades son componentes conectados sobre claves de identidad. Aprende el martes que FILE01 es 172.16.119.10, y los dos registros separados del lunes se convierten en uno en la próxima reconstrucción — sin reescritura, sin marcadores de borrado. Los ids de entidad se direccionan por contenido, así que sobreviven a cada reconstrucción.

Una fusión incorrecta es reparable y reversible. Dile al agente; registra una separación que la derivación respeta — incluso para enlaces afirmados transitivamente.

reprocess --model <better> es seguro. Se añaden nuevas observaciones, nunca se sustituyen, y tus hallazgos y tareas no se tocan. Captura de forma barata y local ahora; mejora la extracción después.


Medido, no asumido

Tres preguntas de diseño se resolvieron mediante medición durante la construcción. Reproduce con los scripts en bench/.

Índice de búsqueda — trigrama, no un tokenizador de palabras afinado. Sobre 20 consultas representativas contra salida real de herramientas:

índice

coincidencias

unicode61 + pentest tokenchars

5 / 20

trigram

20 / 20

Ampliar tokenchars lo suficiente para mantener 172.16.119.10 junto también pega INLANEFREIGHT\fiona:Summer2026! en un solo token, así que buscar solo la contraseña no encuentra nada.

Volumen de captura: la normalización elimina el ruido, no la cantidad.

carga de trabajo

bruto

normalizado

reducción

línea de progreso reescribiéndose a sí misma

247,637

347

714×

aplicación de pantalla completa

7,731

209

37×

volcado de desplazamiento grande (find)

178,022

173,977

Un find de 4,000 líneas es todo contenido real y pasa completo. Por eso la extracción local-primero es esencial, no simplemente prudente.

Modelo — qwen2.5-coder:7b. Evaluado según si los secretos, hashes y direcciones vuelven byte-idénticos:

modelo

recuperación literal

recuperación de tipo

errores de resultado de autenticación

qwen2.5-coder:7b

83%

90%

0

llama3.2:3b

39%

60%

2

El modelo 3B no devolvió nada en absoluto para ambos casos de prueba de autenticación y se equivocó dos veces en éxito-vs-fallo — el único error que este sistema no debe cometer en silencio.


Herramientas MCP

Doce de lectura, tres de escritura. La inflación del número de herramientas degrada la capacidad de un agente para elegir la correcta, por lo que los recuentos en get_engagement_summary llevan las pistas que de otro modo requerirían una herramienta cada una.

lectura

get_engagement_summary

recuentos que revelan qué preguntar a continuación — empieza aquí

list_entities · get_entity

hosts, servicios, identidades, credenciales, recursos compartidos, artefactos

search · get_segment

encontrar una cadena exacta; leer la fuente de la que proviene

get_provenance

rastrear cualquier hecho hasta su segmento, panel, marca de tiempo y rango de bytes

get_recent_activity

lo que sucedió, lo más reciente primero

list_auth_attempts

qué funcionó dónde, y qué no

list_findings · get_finding · list_tasks

list_engagements

escritura

record_observation

notas, hipótesis, hechos vistos fuera del terminal, correcciones

update_finding · update_task

confirmar, rechazar, descartar

Las escrituras siempre se atribuyen al operador o al agente — nunca a la extracción, y el llamante no puede afirmar lo contrario. Ninguna ruta de escritura puede alterar un segmento.


Desarrollo

python -m venv .venv && .venv/bin/pip install -e '.[dev]'
.venv/bin/python -m pytest tests/ -q
.venv/bin/ruff check src tests bench

CI ejecuta la suite en Python 3.11 hasta 3.14, instala tmux para que las pruebas de aceptación con tmux real realmente se ejecuten en lugar de omitirse, ejecuta lint y verifica que la wheel se construya con sus metadatos de licencia intactos.

296 pruebas, incluyendo un ensayo de aceptación completo contra tmux real con un shell inverso anidado, un cliente MCP real sobre stdio, acceso concurrente real entre el grabador y el servidor, y una verificación AST de que solo el módulo proveedor de modelos puede alcanzar una red.

tests/test_capture.py       capture, segmentation, rotation, pause, restart
tests/test_normalize.py     terminal emulation, both implementations
tests/test_extract.py       the extraction contract and the verbatim guard
tests/test_derive.py        union-find correlation, merges, corrections
tests/test_mcp.py           tool surface, bounds, attribution
tests/test_acceptance.py    end-to-end rehearsal in real tmux
tests/test_passivity.py     proves the recorder never touches a target

Las decisiones de diseño, especificaciones y el razonamiento detrás de ellas viven en openspec/changes/add-pentest-recorder-mcp/.

Alcance

Deliberadamente no construido: pentesting autónomo, explotación automática, un terminal o VM de reemplazo, un panel web, una base de datos de grafos, colaboración multiusuario, transporte MCP de red, o un analizador por herramienta.

El grabador captura y organiza. El servidor MCP expone. El agente razona. Esos permanecen separados.

Licencia

MIT — © 2026 Luciano Engel.

Úsalo, hazle fork, publícalo. Viene sin garantía, lo cual importa más de lo habitual aquí: esta herramienta almacena credenciales en texto plano y datos de clientes en disco. Lee Dónde viven tus datos y Cuándo los datos salen de la máquina antes de apuntarlo a un compromiso real.

Install Server
A
license - permissive license
A
quality
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • F
    license
    Not graded
    quality
    C
    maintenance
    An MCP server for EMBA firmware analysis that exposes structured security findings and tools to LLMs. It enables users to programmatically query, reason over, and correlate firmware analysis results such as kernel details, SBOMs, and attack paths.
    6
  • A
    license
    Not graded
    quality
    D
    maintenance
    MCP server that enables AI agents to run fully interactive SSH sessions (via tmux) and execute commands like a human operator, with persistent sessions and multiple concurrent connections.
    6
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    MCP server that provides programmatic access to the SOLVE-IT digital forensics knowledge base, enabling LLMs to query, navigate, and search forensic techniques, weaknesses, mitigations, objectives, and citations.
    1
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    Records your terminal sessions per command (PTY + OSC 133) into local SQLite, so AI agents can search, retrieve, and diff what commands actually printed. Secret redaction is applied by default to everything served over MCP.
    4
    6
    MIT

View all related MCP servers

Related MCP Connectors

  • Search your AI chat history (ChatGPT, Claude, Codex) from any MCP client. Remote, private, read-only

  • Agent-native MCP server over the public saagarpatel.dev corpus. Read-only, stateless.

  • Agentic search over your Dewey document collections from any MCP-compatible client.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/lucianoengel/pentest-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server