Não pergunte ao modelo se o agente fez bem o seu trabalho

Não pergunte ao modelo se o agente fez bem o seu trabalho

Operamos um agente ofensivo contra alvos reais de múltiplas indústrias na América Latina: bancos, fintechs, marketplaces, telecomunicações, seguros, varejo e outros setores. Desde que começamos, executamos mais de 250 Threat Emulations sobre mais de 100 targets reais, e analisamos cada execução uma a uma.

Esse processo nos levou a uma conclusão que acabou sendo mais importante do que esperávamos: avaliar um agente ofensivo não consiste apenas em medir quais vulnerabilidades ele encontra. O problema realmente difícil é entender quando falha, onde falha e por quê.

Este artigo reúne o que aprendemos tentando responder a essa pergunta: os padrões de erro que encontramos, como começamos a medi-los, e as decisões que fomos tomando para construir um agente cada vez mais confiável.

O problema que aparece quando o agente já funciona

Fazer um agente ofensivo rodar já não é a parte mais difícil. A parte difícil vem depois: ele entrega um relatório, e entender onde ele falhou é muito mais custoso do que parece. Peguemos a frase mais comum de todas: "sem achados." Ela pode significar cinco coisas completamente diferentes.

O alvo estava fora do ar durante a execução. Um WAF bloqueou o scanner no primeiro minuto. As credenciais que demos não funcionavam. O container não tinha o binário que o próprio agente invocava. Ou o agente tentou de verdade e não havia nada.

Cinco causas, cinco donos diferentes, e uma única frase no relatório. Quatro delas não são culpa do agente: três se resolvem gerenciando o acesso com o cliente e uma com um Dockerfile. Confundi-las tem dois custos, e um é muito mais grave que o outro. Internamente, você pede à equipe que desenvolve o agente para melhorar algo que já funcionou bem, enquanto o problema real continua intacto. Externamente, o custo é de outra magnitude: dizer a um cliente "o agente não encontrou nada" quando a verdade é "o agente nunca conseguiu entrar." Essa empresa fica tranquila. Fecha o relatório, arquiva o achado vazio e continua operando sobre uma superfície que ninguém nunca testou, convencida de que sim. Não é um relatório incompleto: é uma falsa sensação de segurança, que é pior do que não ter feito nada, porque adia a decisão de investir em revisar aquilo.

Para o time da Strike, é exatamente isso que está em jogo. Não queremos vender um agente que roda, devolve "sem achados" e aí o serviço termina. Hoje qualquer um pode escolher entre dezenas de agentes ofensivos — um open source, outro que custa dez dólares por mês — e todos vão entregar um relatório. A pergunta que muito poucos respondem bem é a única que importa quando esse relatório vem vazio: como você sabe que realmente não havia nada?

É aí que colocamos o trabalho, e por isso a pergunta que nos organiza não é "o agente encontrou algo?" É esta: re-executar recuperaria valor, e quem tem que consertar o quê?

A hipótese que precisava ser descartada: pedir a análise para um LLM

É a abordagem à qual provavelmente qualquer equipe chegaria primeiro: pegar o log da execução, passar para um modelo e pedir o diagnóstico. Nós chegamos até lá, e era a hipótese mais razoável, então a avaliamos a sério antes de construir outra coisa.

Não a descartamos por intuição. Nós a instrumentamos, rodamos sobre nosso corpus e confrontamos suas conclusões com a telemetria real do agente. É isso que nos permite explicar com números por que ela não é suficiente, e são três razões distintas, não uma.

O log não cabe, e o que cabe não é tudo do agente. Um modelo tem um limite de quanto texto pode receber de uma vez. Nossos logs têm uma mediana de 6.024 linhas e o maior chega a 73.428, então quase nunca cabem: é preciso cortá-los antes de passar. Medido sobre todo o corpus, de 624 MB de logs o modelo consegue ver 46 MB. 7,4%. Menos de um em cada quinze logs entra completo, e nos maiores o modelo lê cerca de 1%.

O problema não é só quanto se perde, mas que você não sabe qual pedaço foi lido. E como o corte depende do tamanho do arquivo, também não é o mesmo pedaço entre duas execuções diferentes. Pior: boa parte do que entra não é trabalho do agente. Medimos que 24,8% do corpus é lixo que o agente copiou do site do cliente, código JavaScript comprimido e HTML que ele capturou e despejou no log sem necessidade. No nosso log maior, de 19,8 MB, quatro linhas sozinhas somam 14,9 MB. O modelo cobra para ler isso. E a métrica de cobertura ficava corrompida de um jeito quase cômico: "lemos 100% do log" melhorava quanto mais o agente sujava o próprio log.

Adivinhe o que já está medido. Este foi o achado que mais mudou nosso design. Estávamos pedindo ao modelo que julgasse três coisas: se a execução foi eficiente, se desperdiçou tokens e se travou sem avançar. Depois comparamos cada veredito dele com a telemetria exata daquela mesma execução.

O modelo concluiu "eficiência: média" sobre a execução mais cara de todo o lote: US$ 116, 2 horas, 345 ferramentas. Outra que custou US$ 79 foi classificada como "boa." Ele disse "desperdiçou tokens" sobre uma execução de US$ 0,93. Na de US$ 79 disse que não houve desperdício. Disse "travou sem avançar" sobre uma execução que terminou normalmente. E à única que de fato esgotou o teto de passos, não colocou o rótulo.

Os três veredictos não estavam simplesmente errados: estavam invertidos. E o padrão se repetiu no conjunto completo de rótulos qualitativos que o modelo produziu: 8 de 9 não separavam nada, e 3 apontavam na direção contrária — a execução marcada com o defeito media melhor do que a que não tinha.

Mas o importante não é que o modelo errasse. É por que ele nem deveria estar ali, para começo de conversa. O agente já escreve esses números sozinho, exatos, no próprio log. Quanto custou, quanto durou, quantos passos deu, quantas ferramentas invocou: ele vai anotando isso enquanto trabalha, num formato que uma máquina lê sem ambiguidade. Não há nada a interpretar. Está tudo ali.

E o que fazíamos era isto: apagar essas linhas para que o log coubesse no orçamento do modelo, e depois pagar um dólar e vinte minutos por execução para que o modelo estimasse esses mesmos números lendo o pedaço que sobrou. Jogávamos fora o dado exato e comprávamos uma aproximação pior.

Quando vimos isso escrito assim, a decisão ficou óbvia: isso não se pergunta a um modelo, se lê.

A citação correta com a conclusão errada. Auditamos uma leitura completa, linha por linha. Em honestidade, impecável: 11 de 11 citações existiam literalmente no log, zero inventadas. Ainda assim, em 2 de 7 casos o diagnóstico estava errado.

Essa diferença foi a mais difícil de enxergar: um modelo não inventar a citação não significa que ele entendeu o que citou. E o diagnóstico não é um detalhe, porque é dele que sai a ação. Se ele diz "a execução saiu vazia", o correto é rodar de novo. Se diz "um WAF bloqueou o scanner", o correto é pedir ao cliente que libere o acesso. Com o diagnóstico errado, o sistema recomenda rodar de novo contra um WAF que vai bloquear outra vez: o conselho exatamente oposto ao correto.

O caso extremo: numa Threat Emulation contra um banco, a leitura declarou como crítico que um antibot havia bloqueado o login, citando um reCAPTCHA encontrado no início do log. Como o arquivo não cabe de uma vez, ele é lido por trechos, e três trechos à frente, o mesmo arquivo dizia: "the login succeeded via reCAPTCHA … now authenticated as user." A afirmação ficou contradita pelo próprio log. Testamos com dois modelos diferentes sobre o arquivo idêntico, um vinte vezes mais caro, e nenhum dos dois juntou as duas pontas: cada um leu seu trecho e tirou sua conclusão. O defeito é do método, não do modelo: um sistema que lê em partes e nunca confronta uma parte com as outras vai afirmar coisas que sua própria evidência refuta, com qualquer modelo e a qualquer preço.

Separar medir de narrar

A regra que organizou tudo o mais: o que é mensurável se mede sobre o log completo e sem LLM. O LLM só narra sobre evidência já extraída e citada.

Dito de outro modo: "não inventar" deixa de ser uma instrução no prompt e passa a ser uma propriedade da arquitetura.

A camada determinística roda 18 detectores sobre o log completo, sem LLM. Custo: zero, roda sempre. Cada achado cita linha e byte exatos. Se um detector não consegue citar, não reporta. Reprodutível: duas passagens sobre o mesmo log dão o mesmo resultado.

A camada de leitura roda o LLM sobre o log inteiro, em partições. Mediana de custo: aproximadamente US$ 11 por Threat Emulation. Cada citação é verificada contra o log; se não aparece literalmente, é descartada. É um instrumento de descoberta, não de produção.

Três detalhes de design que acabaram sendo mais importantes do que pareciam.

1. A verificação de citações não é um adorno: é o que torna a camada paga utilizável. Cada afirmação do modelo precisa vir com um trecho que apareça literalmente na partição que acabou de ler, e conferimos isso por programa. Medido sobre as leituras acumuladas: de 759 afirmações com citação, 297 traziam uma citação que não aparece no texto — 39% — e foram descartadas automaticamente. Metade dessas se concentra no vocabulário livre, quando o modelo inventa um rótulo novo em vez de usar um do catálogo. Sem esse filtro, quatro em cada dez afirmações entrariam na base como se fossem evidência.

2. Para o prompt vão fatos, nunca veredictos. A camada de leitura recebe a telemetria medida, mas nunca o que um detector concluiu. Se o modelo sabe o que um detector concluiu, qualquer coisa que ele diga que coincida deixa de ser evidência e passa a ser eco.

3. A camada gratuita não é a versão pobre da paga. Isso nos surpreendeu: medimos que a camada determinística encontra mais classes de defeito do que a camada com LLM em 4 de cada 10 execuções. Não é um superconjunto — cada uma vê coisas que a outra não vê. Há uma razão estrutural: muitos defeitos são propriedades do log completo, e uma página não consegue evidenciar o número de páginas do livro.

E quando as duas camadas se combinam bem, o achado caro se paga uma única vez. Em 4 de 8 Threat Emulations de clientes diferentes, a camada com LLM encontrou a mesma linha: o binário do navegador não existia na imagem. Convertido em detector gratuito e medido sobre todo o corpus, estava presente em 66 de 180 execuções. Nosso detector anterior via apenas 14, era cego por construção, porque ancorava em erros de shell e o navegador é invocado via biblioteca. Cinquenta e duas execuções com uma imagem que nenhum detector via. O conserto era uma linha de Dockerfile.

Quando toda a instrumentação por navegador de um agente está morta, a conclusão não é que o agente seja ruim: é que ele está cego. E esse diagnóstico, uma vez comprado, já cobre de graça cada execução nova.

Categorizar: o que quebrou, quem conserta

Saber que "o agente falhou" não serve para nada. O que muda uma decisão é saber qual subsistema quebrou, porque disso depende quem conserta e com o quê.

A distinção mais útil que encontramos é entre ferramenta e ambiente. Parecem a mesma coisa — nos dois casos "uma tool não funcionou" —, e não são: o fix de "ferramenta" é código; o de "ambiente" é um Dockerfile. Misturá-los manda a equipe procurar no lugar errado, e foi exatamente o que aconteceu com um módulo de descoberta de conteúdo durante meses.

Hoje classificamos todo defeito em três eixos ortogonais — o que quebrou, quem conserta e que ação habilita — sobre nove subsistemas e 32 classes: acesso, ferramenta, observabilidade, memória, cobertura, harness, entregável, ambiente e provedor. O ganho aparece quando você compara empresas. Dois clientes com oito Threat Emulations fracas cada um: em um o problema se concentra em entregável — o agente trabalhou, mas o relatório não reflete isso; no outro, em memória. São duas conversas diferentes com duas equipes diferentes. Antes, os dois recebiam a mesma mensagem genérica.

O que de fato podemos afirmar a um cliente

Medir assim também obriga a ser explícito sobre os limites. E são os limites que nos permitem dizer algo verificável.

"Sem achados" não é o mesmo que "seguro." Não existe denominador: não existe o número total de vulnerabilidades que um alvo tem. Por isso não afirmamos cobertura, e sim algo menor e verificável: qual superfície ficou descoberta e sem ataque. As vulnerabilidades que ninguém viu não são mensuráveis, e apresentá-las como cobertas seria inventar.

Por isso, cada Threat Emulation também traz um bloco com o que não se pode concluir sobre ela, e nunca devolve uma lista vazia: uma lista vazia seria lida como "não há nada que não saibamos", o que nunca é verdade.

Com isso em mãos, quando um banco ou um e-commerce nos pergunta "vocês encontraram alguma coisa?", a resposta "não" deixa de ser uma palavra isolada. Junto com esse "não" podemos colocar na mesa quatro coisas: qual superfície foi descoberta e qual parte recebeu tráfego de ataque real, com a linha e o byte do log onde isso consta; o que ficou sem ser testado, declarado e não estimado, para que seja uma lacuna conhecida e não uma suposição; o que não pôde ser alcançado e por quê — um WAF que bloqueou o scanner, credenciais que não funcionavam, um host fora do ar — para que "não encontramos nada" nunca se confunda com "não conseguimos entrar"; e o que não podemos afirmar sobre aquela execução em particular.

Um "não há achados" sem esse respaldo não é um resultado. É uma ausência de informação com formato de resultado.

E aqui está o ponto incômodo da nossa indústria. Toda semana aparece um agente novo prometendo cobrir toda a sua plataforma. A pergunta que quase nunca é respondida não é quanto ele encontra, e sim como ele sabe o que deixou de olhar. Nós nos dedicamos a medir essa segunda parte, e a mostrá-la mesmo quando o número não nos favorece.

Isso inclui auditar nossas próprias medições. Nosso detector de cobertura chegou a reportar que o agente havia deixado 92% da superfície sem atacar numa Threat Emulation. Ao revisar contra o log bruto, o número real era 5%: ele não estava contando como ataque o tráfego que o agente manda a partir do navegador, que era justamente sua ferramenta principal. Um número de cobertura suspeitosamente alto acabou sendo a assinatura do falso positivo, não a do problema grave.

Não pretendemos ter o melhor analisador do mundo. O que sustentamos é mais modesto e, acreditamos, mais útil: preferimos entregar a um cliente um número menor e verificável do que um número grande sem respaldo. Um percentual de cobertura inventado tranquiliza todo mundo até o dia do incidente.

Por isso, para nós, a validação precisa ser híbrida por design. Não porque o humano seja mais rápido — não é —, mas pela divisão do que cada um pode afirmar: a máquina mede o mensurável sobre a evidência completa; o modelo narra e descobre sobre evidência já citada; e uma pessoa decide o que aquilo significa. Isso também define como estamos organizados: o time de hacking e um time independente de inteligência artificial olham para a mesma execução a partir de lados diferentes, porque quem constrói o agente não pode ser quem mede se o agente fez o seu trabalho.

Um dado observado vale mais do que um declarado. É o princípio do projeto.

Escrevemos isso porque suspeitamos que qualquer equipe que opere agentes em produção, ofensivos ou não, vai esbarrar em paredes parecidas: o log não cabe, você não sabe o que foi lido, adivinhe o que já está medido, e a citação correta pode vir com a conclusão errada. No nosso caso, pelo menos, a saída não foi um modelo melhor. Foi separar o que se mede do que se narra, e exigir da própria plataforma a mesma prova que ela exige do agente.

Provavelmente este modelo de observabilidade ainda pode melhorar, e certamente vai melhorar conforme continuarmos acumulando execuções e evidências. Mas há algo especialmente interessante em trabalhar com esse tipo de agente: ninguém tem a última palavra. Não existe hoje uma receita definitiva para construir ou avaliar um agente ofensivo. Nós também não a temos. O que temos é algo que, na prática, acaba sendo igual de valioso: centenas de execuções reais, erros que conseguimos entender, hipóteses que tivemos que descartar e decisões que tivemos que tomar porque os dados não nos deixavam outra opção.