No le preguntes al modelo si el agente hizo bien su trabajo

Operamos un agente ofensivo contra objetivos reales de múltiples industrias en LATAM: banca, fintech, marketplaces, telecomunicaciones, seguros, retail y otros sectores. Desde que empezamos, ejecutamos más de 250 Threat Emulations sobre más de 100 targets reales, y analizamos cada ejecución una por una.
Ese proceso nos llevó a una conclusión que terminó siendo más importante de lo que esperábamos: evaluar un agente ofensivo no consiste solamente en medir qué vulnerabilidades encuentra. El problema realmente difícil es entender cuándo falla, dónde falla y por qué.
Este artículo reúne lo que aprendimos intentando responder esa pregunta: los patrones de error que encontramos, cómo empezamos a medirlos, y las decisiones que fuimos tomando para construir un agente cada vez más confiable.
El problema que aparece cuando el agente ya funciona
Hacer que un agente ofensivo corra ya no es la parte más difícil. La parte difícil llega después: entrega un reporte, y entender en qué falló es mucho más costoso de lo que parece. Tomemos la frase más común de todas: "sin hallazgos." Puede significar cinco cosas completamente distintas.
El target estuvo caído durante la ejecución. Un WAF bloqueó al scanner en el primer minuto. Las credenciales que le dimos no servían. El contenedor no tenía el binario que el propio agente invocaba. O el agente probó de verdad y no había nada.
Cinco causas, cinco dueños distintos, y una sola frase en el reporte. Cuatro de ellas no son culpa del agente: tres se arreglan gestionando el acceso con el cliente y una con un Dockerfile.
Confundirlas tiene dos costos, y uno es mucho más grave que el otro. Hacia adentro, le pedís al equipo que está desarrollando el agente que mejore algo que funcionó bien, mientras el problema real sigue intacto. Hacia afuera, el costo es de otra magnitud: decirle a un cliente "el agente no encontró nada" cuando la verdad es "el agente nunca pudo entrar." Esa empresa se queda tranquila. Cierra el informe, archiva el hallazgo vacío y sigue operando sobre una superficie que nadie probó nunca, convencida de que sí. No es un reporte incompleto: es una falsa sensación de seguridad, que es peor que no haber hecho nada, porque desplaza la decisión de invertir en revisarlo.
Para el equipo de Strike esto es precisamente lo que está en juego. No queremos vender un agente que corre, devuelve "sin hallazgos" y ahí se termina el servicio. Hoy cualquiera puede elegir entre decenas de agentes ofensivos, uno open source, otro que cuesta diez dólares al mes, y todos van a entregar un reporte. La pregunta que muy pocos responden bien es la única que importa cuando ese reporte viene vacío: ¿cómo sabés que de verdad no había nada?
Ahí es donde ponemos el trabajo, y por eso la pregunta que nos organiza no es "¿el agente encontró algo?" Es esta: ¿re-ejecutar recuperaría valor, y quién tiene que arreglar qué?
La hipótesis que había que descartar: pedirle el análisis a un LLM
Es el enfoque al que probablemente llegaría primero cualquier equipo: agarrar el log de la ejecución, pasárselo a un modelo y pedirle el diagnóstico. Nosotros llegamos ahí, y era la hipótesis más razonable, así que la evaluamos en serio antes de construir otra cosa
No la descartamos por intuición. La instrumentamos, la corrimos sobre nuestro corpus y contrastamos sus conclusiones contra la telemetría real del agente. Eso es lo que nos deja explicar con números por qué no alcanza, y son tres razones distintas, no una.
1. El log no entra, y lo que entra no es todo del agente. Un modelo tiene un límite de cuánto texto puede recibir de una vez. Nuestros logs tienen una mediana de 6.024 líneas y el más grande llega a 73.428, así que casi nunca caben: hay que recortarlos antes de pasarlos. Medido sobre todo el corpus, de 624 MB de logs el modelo alcanza a ver 46 MB. El 7,4%. Menos de uno de cada quince logs entra completo, y en los más grandes el modelo lee cerca del 1%.
El problema no es solo cuánto se pierde, sino que no sabés qué pedazo leyó. Y como el recorte depende del tamaño del archivo, tampoco es el mismo pedazo entre dos ejecuciones distintas.
Peor: buena parte de lo que sí entra no es trabajo del agente. Medimos que el 24,8% del corpus es basura que el agente copió del sitio del cliente, código JavaScript comprimido y HTML que capturó y volcó al log sin necesidad. En nuestro log más grande, de 19,8 MB, cuatro líneas solas suman 14,9 MB. El modelo cobra por leer eso. Y la métrica de cobertura quedaba corrompida de una forma casi cómica: "leímos el 100% del log" mejoraba cuanto más ensuciaba el agente su propio log.
2. Adivina lo que ya está medido. Este fue el hallazgo que más nos cambió el diseño. Le estábamos pidiendo al modelo que juzgara tres cosas: si la ejecución fue eficiente, si desperdició tokens y si se quedó sin avanzar. Después comparamos cada veredicto suyo contra la telemetría exacta de esa misma ejecución.
El modelo concluyó "eficiencia: media" sobre la ejecución más cara de todo el lote: $116, 2 horas, 345 herramientas. Otra que costó $79 la calificó de "buena." Dijo "desperdició tokens" sobre una ejecución de $0,93. En la de $79 dijo que no hubo desperdicio. Dijo "se quedó sin avanzar" sobre una ejecución que terminó con normalidad. Y a la única que de verdad agotó el techo de pasos no le puso la etiqueta.
Los tres veredictos no estaban simplemente errados: estaban al revés. Y el patrón se repitió en el conjunto completo de etiquetas cualitativas que produjo el modelo: 8 de 9 no separaban nada, y 3 apuntaban en la dirección contraria, la ejecución marcada con el defecto medía mejor que la que no lo tenía.
Pero lo importante no es que el modelo se equivocara. Es por qué no tenía que haber estado ahí en primer lugar. El agente ya escribe esos números él mismo, exactos, en su propio log. Cuánto costó, cuánto duró, cuántos pasos dio, cuántas herramientas invocó: los va anotando a medida que trabaja, en un formato que una máquina lee sin ambigüedad. No hay nada que interpretar. Están ahí.
Y lo que hacíamos era esto: borrar esas líneas para que el log entrara en el presupuesto del modelo, y después pagar un dólar y veinte minutos por ejecución para que el modelo estimara esos mismos números leyendo el pedazo que quedaba. Tirábamos el dato exacto y comprábamos una aproximación peor.
Cuando lo vimos escrito así, la decisión se volvió obvia: eso no se le pregunta a un modelo, se lee.
3. La cita correcta con la conclusión equivocada. Auditamos una lectura completa, línea por línea. Sobre honestidad, impecable: 11 de 11 citas existían literalmente en el log, cero inventadas. Y aun así, en 2 de 7 casos el diagnóstico era el equivocado.
Esa diferencia es la que más nos costó ver: que un modelo no invente la cita no significa que entienda lo que citó. Y el diagnóstico no es un detalle, porque de él sale la acción. Si dice "la ejecución salió vacía", lo que corresponde es volver a correrla. Si dice "un WAF bloqueó al scanner", lo que corresponde es pedirle al cliente que nos habilite el acceso. Con el diagnóstico equivocado, el sistema recomienda volver a correr contra un WAF que va a bloquear otra vez: el consejo exactamente opuesto al correcto.
El caso extremo: en una Threat Emulation contra un banco, la lectura declaró crítico que un antibot había bloqueado el login, citando un reCAPTCHA que encontró al principio del log. Como el archivo no cabe de una vez, se lee por tramos, y tres tramos más adelante, el mismo archivo decía: "the login succeeded via reCAPTCHA … now authenticated as user." La afirmación quedó contradicha por su propio log. Lo probamos con dos modelos distintos sobre el archivo idéntico, uno veinte veces más caro, y ninguno de los dos juntó las dos puntas: cada uno leyó su tramo y sacó su conclusión. El defecto es del método, no del modelo: un sistema que lee por partes y nunca contrasta una parte contra las otras va a afirmar cosas que su propia evidencia refuta, con cualquier modelo y a cualquier precio.
Separar medir de narrar
La regla que ordenó todo lo demás: lo medible se mide sobre el log completo y sin LLM. El LLM solo narra sobre evidencia ya extraída y citada.
Dicho de otro modo: "no inventar" deja de ser una instrucción en el prompt y pasa a ser una propiedad de la arquitectura.
La capa determinística corre 18 detectores sobre el log completo, sin LLM. Costo: cero, corre siempre. Cada hallazgo cita línea y byte exactos. Si un detector no puede citar, no reporta. Reproducible: dos pasadas sobre el mismo log dan lo mismo.
La capa de lectura corre el LLM sobre el log entero, en particiones. Mediana de costo: aproximadamente $11 por Threat Emulation. Cada cita se verifica contra el log; si no aparece literal, se descarta. Es un instrumento de descubrimiento, no de producción.
Tres detalles de diseño que resultaron más importantes de lo que parecían.
La verificación de citas no es un adorno: es lo que hace usable la capa paga. Cada afirmación del modelo tiene que venir con un fragmento que aparezca literal en la partición que acaba de leer, y lo comprobamos por programa. Medido sobre las lecturas acumuladas: de 759 afirmaciones con cita, 297 traían una cita que no aparece en el texto, el 39%, y se descartaron automáticamente. La mitad de esas se concentran en el vocabulario libre, cuando el modelo inventa una etiqueta nueva en vez de usar una del catálogo. Sin ese filtro, cuatro de cada diez afirmaciones entrarían a la base como si fueran evidencia.
Al prompt van hechos, nunca veredictos. La capa de lectura recibe la telemetría medida, pero jamás lo que concluyó un detector. Si el modelo sabe qué concluyó un detector, cualquier cosa que diga que coincida deja de ser evidencia y pasa a ser eco.
La capa gratis no es la versión pobre de la paga. Esto nos sorprendió: medimos que la capa determinística encuentra más clases de defecto que la capa con LLM en 4 de cada 10 ejecuciones. No es un superconjunto, cada una ve cosas que la otra no. Hay una razón estructural: muchos defectos son propiedades del log completo, y una página no puede evidenciar el número de páginas del libro.
Y cuando las dos capas se combinan bien, el hallazgo caro se paga una sola vez. En 4 de 8 Threat Emulations de clientes distintos, la capa con LLM encontró la misma línea: el binario del navegador no existía en la imagen. Convertido en detector gratis y medido sobre todo el corpus, estaba en 66 de 180 ejecuciones. Nuestro detector anterior veía apenas 14, era ciego por construcción, porque anclaba en errores de shell y el navegador se invoca por librería. Cincuenta y dos ejecuciones con una imagen que ningún detector veía. El arreglo era una línea de Dockerfile.
Cuando toda la instrumentación por browser de un agente está muerta, la conclusión no es que el agente sea malo: es que está ciego. Y ese diagnóstico, una vez comprado, ya cubre gratis cada ejecución nueva.
Categorizar: qué se rompió, quién lo arregla
Saber que "el agente falló" no sirve para nada. Lo que cambia una decisión es saber qué subsistema se rompió, porque de eso depende quién lo arregla y con qué.
La distinción más útil que encontramos es entre herramienta y entorno. Parecen lo mismo, en los dos casos "una tool no funcionó", y no lo son: el fix de "herramienta" es código; el de "entorno" es un Dockerfile. Mezclarlos manda al equipo a buscar en el lugar equivocado, y es exactamente lo que nos pasó con un módulo de descubrimiento de contenido durante meses.
Hoy clasificamos todo defecto en tres ejes ortogonales, qué se rompió, quién lo arregla y qué acción habilita, sobre nueve subsistemas y 32 clases: acceso, herramienta, observabilidad, memoria, cobertura, harness, entregable, entorno y proveedor. El payoff se ve cuando comparás empresas. Dos clientes con ocho Threat Emulations flojas cada uno: en uno el problema se concentra en entregable, el agente trabajó pero el reporte no lo refleja; en el otro, en memoria. Son dos conversaciones distintas con dos equipos distintos. Antes, los dos recibían el mismo mensaje genérico.
Lo que sí podemos afirmarle a un cliente
Medir así también obliga a ser explícito sobre los límites. Y son los límites los que nos habilitan a decir algo verificable.
"Sin hallazgos" no es lo mismo que "seguro." No hay denominador: no existe el número total de vulnerabilidades que un target tiene. Por eso no afirmamos cobertura, sino algo más chico y verificable: qué superficie quedó descubierta y sin atacar. Las vulnerabilidades que nadie vio no son medibles, y presentarlas como cubiertas sería inventar.
Por eso cada Threat Emulation lleva también un bloque con lo que no se puede concluir sobre ella, y nunca devuelve una lista vacía: una lista vacía se leería como "no hay nada que no sepamos", que nunca es cierto.
Con eso en la mano, cuando un banco o un ecommerce nos pregunta "¿encontraron algo?", la respuesta "no" deja de ser una palabra sola. Junto a ese "no" podemos poner sobre la mesa cuatro cosas: qué superficie se descubrió y qué parte recibió tráfico de ataque real, con la línea y el byte del log donde consta; qué quedó sin probar, declarado y no estimado, para que sea una brecha conocida y no un supuesto; qué no se pudo alcanzar y por qué, un WAF que bloqueó al scanner, credenciales que no servían, un host caído, así "no encontramos nada" nunca se confunde con "no pudimos entrar"; y qué no podemos afirmar sobre esa ejecución en particular.
Un "no hay hallazgos" sin ese respaldo no es un resultado. Es una ausencia de información con formato de resultado.
Y acá está el punto incómodo de nuestra industria. Cada semana aparece un agente nuevo que promete cubrir toda tu plataforma. La pregunta que casi nunca se responde no es cuánto encuentra, sino cómo sabe qué dejó sin mirar. Nosotros nos dedicamos a medir esa segunda parte, y a mostrarla incluso cuando el número no nos favorece.
Eso incluye auditar nuestras propias mediciones. Nuestro detector de cobertura llegó a reportar que el agente había dejado el 92% de la superficie sin atacar en una Threat Emulation. Al revisarlo contra el log crudo, el número real era 5%: no estaba contando como ataque el tráfico que el agente manda desde el navegador, que era justamente su herramienta principal. Un número de cobertura sospechosamente alto resultó ser la firma del falso positivo, no la del problema grave.
No pretendemos tener el mejor analizador del mundo. Lo que sostenemos es más modesto y, creemos, más útil: preferimos entregarle a un cliente un número más chico y verificable que uno grande sin respaldo. Un porcentaje de cobertura inventado tranquiliza a todos hasta el día del incidente.
Por eso, para nosotros, la validación tiene que ser híbrida por diseño. No porque el humano sea más rápido, no lo es, sino por el reparto de lo que cada uno puede afirmar: la máquina mide lo medible sobre la evidencia completa; el modelo narra y descubre sobre evidencia ya citada; y una persona decide qué significa. Eso también define cómo estamos organizados: el equipo de hacking y un equipo independiente de inteligencia artificial miran la misma ejecución desde lados distintos, porque quien construye el agente no puede ser quien mide si el agente hizo su trabajo.
Un dato observado le gana a uno declarado. Es el principio del proyecto.
Lo escribimos porque sospechamos que cualquier equipo que opere agentes en producción, ofensivos o no, se va a topar con paredes parecidas: el log no entra, no sabés qué leyó, adivina lo que ya está medido, y la cita correcta puede venir con la conclusión equivocada. Al menos en nuestro caso, la salida no fue un modelo mejor. Fue separar lo que se mide de lo que se narra, y exigirle a la propia plataforma la misma prueba que le exige al agente.
Probablemente este modelo de observabilidad todavía pueda mejorarse, y seguramente lo haga a medida que sigamos acumulando ejecuciones y evidencia. Pero hay algo especialmente interesante en trabajar con este tipo de agentes: nadie tiene la última palabra. No existe hoy una receta definitiva para construir, evaluar un agente ofensivo. Nosotros tampoco la tenemos. Lo que sí tenemos es algo que, en la práctica, termina siendo igual de valioso: cientos de ejecuciones reales, errores que pudimos entender, hipótesis que tuvimos que descartar y decisiones que tuvimos que tomar porque los datos no nos dejaban otra opción.



