Ingest de hallazgos y aspectos
PAT con alcance ingest. El JWT de sesión responde 401. Ingest no crea proyectos ni escenarios: si la identidad no existe, 404. Para alta use Asegurar proyecto. No se envían evidencias binarias.
Identidad del destino
El cliente no envía UUIDs de proyecto. Identifica con:
project_name+project_code— EVA resuelve el código en el tenant del PAT y verifica que el nombre coincida.scenario_key— clave de catálogo (WEB,API,EXTERNAL…). Si hay más de un escenario con esa clave en el proyecto → 409SCENARIO_AMBIGUOUS.
source_system es obligatorio. origin persistido = integration.
El usuario dueño del PAT debe poder crear hallazgos en ese proyecto. Identidad o permiso incorrectos → 404 (no se distingue existencia).
Por qué siempre draft
draft
El hallazgo queda en draft con origin=integration. Ingest no cambia el flujo de revisión en EVA ni crea un job de retest. execution_scope es opcional (cloud | on_prem | manual); si se omite, queda vacío.
Los cuatro métodos
| Método | Ruta | Body | Éxito |
|---|---|---|---|
| JSON hallazgo | POST /api/v1/integrations/ingest/vulnerabilities | JSON | 201 |
| CSV hallazgos | POST /api/v1/integrations/ingest/vulnerabilities.csv | multipart/form-data campo file | 200 parcial |
| JSON aspecto | POST /api/v1/integrations/ingest/positive-aspects | JSON | 201 |
| CSV aspectos | POST /api/v1/integrations/ingest/positive-aspects.csv | multipart/form-data campo file | 200 |
CSV: máximo 200 filas / 1 MB. Respuesta { created, failed, items[] } incluso si todas las filas fallan. No hay 202.
Scope ingest. CSV: campo multipart file. Fixtures en examples/.
Campos
Comunes: project_name, project_code, scenario_key, source_system, assets.
Hallazgos además: title (obligatorio, máx. 200), severity, cvss_score, cvss_vector, cwes, cves, mitre_tactic_ids, mitre_technique_id, tool_id, execution_scope (opcional: cloud | on_prem | manual). Si envía executor, se ignora.
Aspectos además: title, description.
assets en JSON es una unión: string o {value, asset_type?, label?, meta?}. Se pueden mezclar. Máx. 100. value máx. 1000, label máx. 300. Tipo inválido → 422. Si asset_type se omite, EVA infiere (IPv4/IPv6 → ip; http(s) en escenario API → endpoint, si no url; si no es URL/IP, el mapa del escenario; fallback host). Un tipo explícito siempre gana. Match-or-create por proyecto + escenario + tipo + valor; si ya existe no se pisan label/meta. El GET extract ya lee asset_type / value / label.
CSV: assets separados por ;. Columnas opcionales asset_types y asset_labels (listas ; paralelas). Distinta cardinalidad → 422 en esa fila. Celda de tipo vacía = inferir. meta no va en CSV.
MITRE = tácticas + una técnica.
1. JSON hallazgo
curl -sS -X POST \
-H "Authorization: Bearer ${EVA_INTEGRATION_TOKEN}" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d @examples/ingest_vulnerability.example.json \
"${EVA_API_BASE_URL}/api/v1/integrations/ingest/vulnerabilities"
Fixture: ingest_vulnerability.example.json
{
"project_name": "Banco XYZ 2026",
"project_code": "BNK-1",
"scenario_key": "WEB",
"source_system": "acme-scanner",
"title": "Reflected XSS in login",
"severity": "high",
"cvss_score": 7.5,
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
"cwes": ["CWE-79"],
"cves": [],
"mitre_tactic_ids": ["TA0001"],
"mitre_technique_id": "T1190",
"tool_id": "nuclei",
"assets": [
"https://app.example.com/login",
{
"value": "https://apisux.example.com/v1/users",
"asset_type": "endpoint",
"label": "Users API",
"meta": { "method": "GET", "env": "qa" }
}
],
"executor": "optional-ignored"
}
Esperado: HTTP 201, "contract": "eva.integration.ingest.v1", "origin": "integration", "status": "draft".
2. CSV hallazgos
curl -sS -X POST \
-H "Authorization: Bearer ${EVA_INTEGRATION_TOKEN}" \
-H "Accept: application/json" \
-F "file=@examples/ingest_vulnerability.example.csv" \
"${EVA_API_BASE_URL}/api/v1/integrations/ingest/vulnerabilities.csv"
Fixture: ingest_vulnerability.example.csv
project_name,project_code,scenario_key,source_system,title,severity,cvss_score,cvss_vector,cwes,cves,mitre_tactic_ids,mitre_technique_id,tool_id,assets,asset_types,asset_labels,executor Banco XYZ 2026,BNK-1,WEB,acme-scanner,Reflected XSS in login,high,7.5,CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N,CWE-79,,TA0001,T1190,nuclei,https://app.example.com/login,,, Banco XYZ 2026,BNK-1,API,acme-scanner,Broken object level authorization,high,8.1,CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N,CWE-639,,TA0001,T1190,nuclei,https://api.example.com/v1/users,endpoint,Users API,
Esperado: HTTP 200 con created / failed / items[] por fila.
3. JSON aspecto positivo
curl -sS -X POST \
-H "Authorization: Bearer ${EVA_INTEGRATION_TOKEN}" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d @examples/ingest_positive_aspect.example.json \
"${EVA_API_BASE_URL}/api/v1/integrations/ingest/positive-aspects"
Fixture: ingest_positive_aspect.example.json
{
"project_name": "Banco XYZ 2026",
"project_code": "BNK-1",
"scenario_key": "WEB",
"source_system": "acme-scanner",
"title": "TLS 1.2 enforced on public login",
"description": "The public login endpoint rejects TLS 1.0 and 1.1.",
"assets": [
"https://app.example.com/login",
{
"value": "https://apisux.example.com/v1/users",
"asset_type": "endpoint",
"label": "Users API",
"meta": { "method": "GET", "env": "qa" }
}
]
}
Esperado: HTTP 201, mismo contrato e origin=integration.
4. CSV aspectos positivos
curl -sS -X POST \
-H "Authorization: Bearer ${EVA_INTEGRATION_TOKEN}" \
-H "Accept: application/json" \
-F "file=@examples/ingest_positive_aspect.example.csv" \
"${EVA_API_BASE_URL}/api/v1/integrations/ingest/positive-aspects.csv"
Fixture: ingest_positive_aspect.example.csv
project_name,project_code,scenario_key,source_system,title,description,assets,asset_types,asset_labels Banco XYZ 2026,BNK-1,WEB,acme-scanner,TLS 1.2 enforced on public login,The public login endpoint rejects TLS 1.0 and 1.1.,https://app.example.com/login,,
Comprobaciones
| Caso | Esperado |
|---|---|
| JWT de sesión en Bearer | 401 TOKEN_TYPE_NOT_ALLOWED |
PAT solo-extract | 403 INTEGRATION_SCOPE_DENIED |
IP fuera de allowed_cidrs | 403 INTEGRATION_IP_DENIED |
| Más de 60 POST/min del mismo PAT | 429 INTEGRATION_RATE_LIMITED + Retry-After |
| CSV de 200 filas | Cuenta como 1 POST en el cupo |
scenario_key ambiguo | 409 SCENARIO_AMBIGUOUS |
| Proyecto/permiso incorrecto | 404 |
Body inválido / asset_type desconocido / CSV con distinta aridad assets vs asset_types | 422 |
El cupo de 60 peticiones/minuto es por PAT y superficie ingest (independiente de extract). Un CSV cuenta como 1 POST. Retest no lo comparte. Rangos de IP en notación CIDR (10.0.0.0/8), no con guion.