API de retest
Contrato para el runner: poll, claim, heartbeat y result. El job es opaco. El runner no cambia el estado del hallazgo. La imagen se publica en un registro; ver Runner.
POST …/result guarda el outcome del job y puede adjuntar evidencias. No certifica ni transiciona el estado del hallazgo.
Cómo llega un job
EVA encola un job cuando el hallazgo es automatable: tool_id nuclei o nmap, execution_scope cloud u on_prem, y un asset usable (Nuclei: URL/endpoint; Nmap: IP/host). Si falta alguno, no hay job. GET pending no crea filas: solo entrega el job pending más antiguo o null. Un job en pending siempre trae target.value no vacío.
Rutas
Base: {EVA_API_BASE_URL}/api/v1/integrations
| Método | Path | Scope | Acción |
|---|---|---|---|
GET | /retests/pending | retest_read | Un job pending existente o null. No crea filas. |
POST | /retests/jobs/{id}/claim | retest_write | Toma el job. Lease 300 s. |
POST | /retests/jobs/{id}/heartbeat | retest_write | Renueva lease. claimed → running. |
POST | /retests/jobs/{id}/result | retest_write | Outcome + evidencias. No cambia status del hallazgo. |
{id} es UUID del job. Jobs de otro tenant → 404 JOB_NOT_FOUND. No hay listado de vulnerabilidades, SQL ni visor de logs.
Constantes: RETEST_JOB_CONTRACT = "eva.integration.retest.v1", LEASE_SECONDS = 300.
Query de pending: tool_id + execution_scope (obligatorios). site_key es opcional: si no se envía, la cola es la de hoy (todos los proyectos del tenant). El tenant sale del PAT. Varios runners pueden competir por el mismo tool+scope → 409 JOB_ALREADY_CLAIMED.
pending, claim, heartbeat y result no aplican allowed_cidrs ni el rate limit de 60/min. Un 403 INTEGRATION_IP_DENIED o 429 INTEGRATION_RATE_LIMITED no pertenece a esta superficie. Solo PAT + scope. El envelope del job no cambia.
Job opaco
El JSON nunca incluye: title, description, impact, recommendation, CVSS, CWEs, catálogo, severity, emails, URIs de evidencias previas, ni nombres de usuario.
Descargue: pending.example.json (Nuclei), pending.nmap.example.json (Nmap), pending.empty.json (sin trabajo). Todos los fixtures: examples/.
{
"contract": "eva.integration.retest.v1",
"job": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "pending",
"tool_id": "nuclei",
"execution_scope": "on_prem",
"lease_expires_at": null,
"runner_id": null,
"recipe": { "kind": "nuclei", "templates": ["http/vulnerabilities"] },
"target": {
"asset_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"asset_type": "url",
"value": "https://app.example"
}
}
}
Sin trabajo: { "contract": "eva.integration.retest.v1", "job": null }. Un job por respuesta, no un array. Status del job: pending | claimed | running | succeeded | failed | expired.
Recetas v1
EVA arma la receta al encolar. El runner no la cambia. Por defecto, Nuclei usa http/vulnerabilities y Nmap puertos 80,443. Si hay un CVE mapeado, pueden ir templates o puertos concretos.
{ "kind": "nuclei", "templates": ["http/vulnerabilities"] }
Nmap:
{ "kind": "nmap", "ports": "80,443" }
Cualquier otro kind → outcome=not_applicable sin error_code.
GET pending
curl -sS \
-H "Authorization: Bearer ${EVA_INTEGRATION_TOKEN}" \
-H "Accept: application/json" \
"${EVA_API_BASE_URL}/api/v1/integrations/retests/pending?tool_id=nuclei&execution_scope=on_prem"
Query obligatoria: tool_id = nuclei | nmap; execution_scope = cloud | on_prem. Valores fuera del set → 422. Query opcional site_key: filtrar solo si el runner la envía; omitida = cola actual.
Pending no reclama el job. Filtro: tenant del PAT, tool_id + execution_scope (y site_key si se envía). No hay query tenant_id.
POST claim
Body: claim.request.json
{ "runner_id": "nuclei-lab-01" }
runner_id: 1–128, charset ^[A-Za-z0-9._:-]+$. Efectos: pending → claimed, lease 300 s.
- Mismo
runner_idsobre job ya claimed/running: idempotente, renueva lease. - Otro
runner_idcon lease vigente → 409JOB_ALREADY_CLAIMED. - Job
succeeded/failed→ 409JOB_ALREADY_FINISHED. - Job
expired→ 409JOB_LEASE_EXPIRED.
POST heartbeat
{ "runner_id": "nuclei-lab-01" }
Debe coincidir el claim holder y el lease vigente. Si claimed, pasa a running. Intervalo sugerido 30 s (menor que 300 s). Respuesta 200:
{
"ok": true,
"status": "running",
"lease_expires_at": "2026-08-21T03:15:00+00:00"
}
POST result
Fixture: result.request.json
{
"runner_id": "nuclei-lab-01",
"outcome": "still_open",
"error_code": null,
"evidences": [
{
"filename": "nuclei.log",
"evidence_type": "log",
"content_base64": "Tm91bmQgdnVsbmVyYWJpbGl0eQo=",
"caption": "nuclei stdout"
}
]
}
| Campo | Regla |
|---|---|
outcome | appears_fixed | still_open | error | not_applicable |
error_code | Obligatorio si outcome=error. Set cerrado abajo. |
evidences | Máximo 3. Filename 1–255 sin / ni ... Tipo log | file. Base64 decodificado 1–262144 bytes. Caption opcional máx. 200. |
outcome=error → job failed. Cualquier otro → job succeeded. El resultado no cambia el estado del hallazgo. La respuesta 200 incluye vulnerability_status de solo lectura (estado actual).
{
"ok": true,
"status": "succeeded",
"outcome": "still_open",
"vulnerability_status": "draft"
}
Error de herramienta: result.error.json
{
"runner_id": "nuclei-lab-01",
"outcome": "error",
"error_code": "target_unreachable",
"evidences": []
}
error_code (set cerrado)
| Código | Cuándo |
|---|---|
auth_failed | El target del job exige autenticación que el tool no tiene. Un 401 de EVA (PAT) no se envía aquí: el runner lo loguea como class local y no llega a result. |
target_unreachable | DNS, timeout o conexión rehusada hacia el asset, no hacia Flask. |
tool_crash | Nuclei/Nmap exit ≠ 0 no clasificable |
timeout | El runner mató el proceso |
Cualquier otro error_code → 422. Un fallo de red hacia EVA no se reporta como error_code: el runner reintenta. Operación: Runner → Health.
Interpretación Nuclei / Nmap → outcome
Nuclei: exit 0 con match → still_open. Exit 0 sin matches → appears_fixed. Timeout → error/timeout. Unreachable → error/target_unreachable. Crash → error/tool_crash.
Nmap: al menos un puerto de la receta open → still_open. Todos closed/filtered y el host respondió → appears_fixed. Host down → target_unreachable.
Esto alimenta el job; no sustituye la validación en EVA.