Imagen publicada linux/amd64

Runner Nuclei / Nmap

El cliente no construye el agente. Recibe una imagen ya publicada en un registro privado, un usuario de pull y un PAT de EVA. Nuclei y Nmap viajan dentro de esa imagen.

Imagen publicada Recibirá un registro, un tag versionado y un usuario de pull. Nuclei y Nmap van dentro de la imagen. El PAT de EVA se inyecta en runtime, no en el build.

Cómo se entrega

01 Entelgy publica

CI construye linux/amd64 y hace push al registry.

02 Registro privado

Tag semver y digest. Credencial de pull, no el PAT.

03 Cliente descarga

docker login y docker pull en on-prem.

04 Contenedor corre

docker run con el secreto EVA inyectado en runtime.

Qué recibeEjemploNotas
Nombre de imagen registry.ejemplo.com/eva/runner-nuclei Placeholder hasta el registry real. También existe …/runner-nmap.
Tag 1.0.0 En producción pinneé además el digest @sha256:….
Login de registry robot / IAM / token de pull Solo lectura del catálogo de imágenes. No autentica EVA.
PAT de EVA eva_pat_… Scopes retest_read + retest_write. Almacén de secretos del cliente.

Haga esto

  • Pull de un tag publicado
  • Correr linux/amd64
  • Inyectar el PAT en runtime
  • Rotar tag y PAT por separado

No haga esto

  • docker build en el Mac o en el servidor
  • Instalar Go, Nuclei o Nmap en el host
  • Meter el PAT en la imagen o en el registry
  • Usar el tag :latest en producción

Arrancar en el cliente

  1. Cree un PAT en Cuenta → Configuración → Integración con retest_read y retest_write. Cópielo una vez.
  2. Guarde el PAT en el almacén de secretos del cliente. El login del registry es otra credencial.
  3. Autentíquese en el registro y descargue la imagen versionada.
  4. Levante el contenedor con las variables de abajo. Compruebe /health (proceso vivo) y /ready (binario + EVA).
pull + run
docker login registry.ejemplo.com

docker pull registry.ejemplo.com/eva/runner-nuclei:1.0.0
docker pull registry.ejemplo.com/eva/runner-nmap:1.0.0

docker run --rm --env-file .env -p 8080:8080 \
  registry.ejemplo.com/eva/runner-nuclei:1.0.0

Sustituya el host y el tag por los que Entelgy le entregue. La plataforma de la imagen de producto es linux/amd64.

Variables de entorno

No meta secretos en la imagen. El .env vive en el host y no se commitea:

EVA_INTEGRATION_TOKEN=
EVA_API_BASE_URL=https://api.ejemplo.com
EVA_TOOL_ID=nuclei
EVA_EXECUTION_SCOPE=on_prem
EVA_RUNNER_ID=nuclei-lab-01
# EVA_SITE_KEY=          # opcional; vacío = cola actual
EVA_HEALTH_ADDR=:8080
EVA_POLL_INTERVAL_SECONDS=15
EVA_TOOL_TIMEOUT_SECONDS=120
EVA_LOG_FORMAT=json
VariableObligatorioValor
EVA_API_BASE_URLBackend Flask, no el BFF de Next
EVA_INTEGRATION_TOKENPAT con retest_read + retest_write
EVA_TOOL_IDnuclei o nmap — debe coincidir con la imagen
EVA_EXECUTION_SCOPEcloud o on_prem
EVA_RUNNER_IDHostname estable, 1–128, A-Za-z0-9._:-
EVA_SITE_KEYNoVacío = cola actual (tenant × tool × scope). Solo si el proyecto tiene site_key.
EVA_HEALTH_ADDRNo:8080
EVA_POLL_INTERVAL_SECONDSNo15
EVA_TOOL_TIMEOUT_SECONDSNo120
EVA_LOG_FORMATNojson (stderr) o text en laboratorio

Health y logs

El health es local al contenedor, no es la API EVA. /health es liveness (el proceso vive). /ready es readiness (binario + EVA). /metrics es texto Prometheus en el mismo puerto (host-only, como /health; no forma parte de eva.integration.retest.v1):

curl -sS http://127.0.0.1:8080/health
curl -sS -o /tmp/ready.json -w "%{http_code}\n" http://127.0.0.1:8080/ready
curl -sS http://127.0.0.1:8080/metrics
{
  "status": "idle",
  "runner_id": "nuclei-lab-01",
  "tool_id": "nuclei",
  "execution_scope": "on_prem",
  "last_error": null,
  "version": "1.0.0",
  "tool_binary_ok": true,
  "eva_reachable": true,
  "last_class": "",
  "last_job_id": "",
  "idle_polls": 0,
  "started_at": "2026-08-21T15:00:00Z"
}

status: idle o running. /health sigue en 200 si EVA está caído; /ready pasa a 503. /ready 200 es la aceptación de enganche: binario en la imagen y Flask alcanzable. No prueba el target del job. Sin el binario de Nuclei/Nmap el agente no hace claim. Docker usa HEALTHCHECK contra /health (eva-runner healthcheck). SIGTERM: si hay scan, cancela el binario y POST result (timeout si el proceso muere); idle, apaga. Un log de evidencia > 256 KiB se recorta a cabeza + cola. El caption es {tool} stdout o {tool} stderr; si hay ambos, hasta dos ítems. Target en blanco: not_applicable sin binario. Heartbeat con lease vencido o 401 aborta el tool y no envía still_open/appears_fixed; un parpadeo de EVA no aborta.

Logs del agente → JSON en stderr (runner_id, tool_id, execution_scope, site_key, class; en un job también job_id, target, recipe, outcome, error_code, reason). Al arrancar: start y watching. Cola vacía: idle (primer poll y cada 20). Nuclei/Nmap → stdout de docker logs, no al body de EVA. El PAT no se loguea. El agente envía User-Agent: eva-runner/{versión} ({tool}; {scope}); EVA lo ignora.

Si el operador ve…SignificaNo es
auth_failed (logs / /ready 503) PAT inválido o revocado. Backoff ~60 s. El target del job caído.
eva_unreachable (logs / /ready 503, /health 200) No llega a EVA (red, DNS, 5xx). Backoff 5→60 s. Un error_code hacia EVA. Nunca se envía en result.
target_unreachable en el result / logs tool Nuclei/Nmap no alcanza el target del job (LAN, DNS, firewall). EVA caído.
idle en logs / idle_polls en /health, /ready 200 El nodo está enganchado. EVA no le ofrece job (cola vacía o site_key distinto). Un contenedor muerto. Eso es /health caído.
reason=template_missing con tool_crash Nuclei no tiene el YAML de la receta. El error_code hacia EVA sigue siendo tool_crash. Target inalcanzable.
reason=empty_target con not_applicable Guardia del runner: target.value en blanco. No lanza Nuclei/Nmap ni envía error_code. Un error_code nuevo. EVA no debería encolar esto.

Red Docker hacia Flask y hacia la LAN

El runner no tiene inventario de hosts. EVA elige target.value al encolar y ese valor no llega vacío. El contenedor solo necesita llegar a Flask (EVA_API_BASE_URL) y a ese valor:

Un bridge aislado puede dejar /ready en 200 (EVA responde) y el scan en target_unreachable.

Ciclo del agente

  1. Preflight: binario en PATH, DNS de EVA_API_BASE_URL y un GET …/retests/pending.
  2. Poll GET …/retests/pending.
  3. Si job != null y el binario está, POST …/claim. Si falta el binario, no reclama.
  4. Ejecuta la receta (Nuclei o Nmap) contra target.value.
  5. Heartbeat mientras corre la herramienta (lease 300 s). Lease/auth/conflicto definitivo aborta el scan y no POST un outcome de éxito.
  6. POST …/result con outcome y hasta 3 evidencias. Caption {tool} stdout o {tool} stderr (dos ítems si hay ambos). Log recortado cabeza+cola si supera 256 KiB. Si EVA parpadea, reintenta 2–3 veces; 409 JOB_ALREADY_FINISHED cuenta como éxito. SIGTERM con tool en vuelo: cancela y POST result.
  7. Si no hay job, duerme EVA_POLL_INTERVAL_SECONDS.

El runner no cambia el estado del hallazgo.

Poll, claim, heartbeat y result no pasan por la allowlist CIDR ni el cupo de 60/min de extract/ingest. Un PAT de runner con allowed_cidrs relleno sigue autenticando estas rutas. Esas barreras son solo extract/ingest.

Qué ve el runner

Solo jobs pending que EVA ya encoló para el tool_id y execution_scope del contenedor (y site_key si se configura). El job incluye target.value y la receta. Cola vacía → espera el siguiente poll.

Tras revocar el PAT, pending y claim responden 401. Pare el contenedor y retire el secreto.

Varios runners

Los tenants están aislados. Dentro de un tenant, todos los proyectos comparten la cola de ese tool_id + execution_scope salvo que el runner envíe site_key (opt-in). Sin EVA_SITE_KEY el comportamiento es el de hoy. Dos contenedores pueden competir: el segundo claim con otro runner_id y lease vigente recibe 409 JOB_ALREADY_CLAIMED.

Red aislada

Si el host no puede alcanzar el registro, Entelgy puede entregar un artefacto OCI (docker save) para docker load en destino. Sigue siendo la misma imagen; no es un binario suelto ni un build en el cliente.

Qué no hace el runner