Fackel: um framework autônomo de pentest baseado em agentes ReAct
Índice
A maioria das ferramentas de automação de pentest codifica a estratégia no código: rode este scanner, parseie aquela saída, alimente o próximo passo. O humano decide a sequência; a ferramenta apenas executa. O Fackel inverte essa relação. O LLM decide o que fazer em seguida — quais ferramentas chamar, como interpretar resultados e quando seguir em frente — enquanto o código impõe segurança, validação e estrutura.
Este post cobre a arquitetura, as decisões de design principais e os trade-offs que surgiram ao construir o Fackel.
O pipeline
O Fackel executa um pipeline de 5 fases, onde cada fase é um nó do LangGraph:
Target → OSINT → Approval Gate → Port Scan → Vuln Scan → Triage → Report
O agente de OSINT tem 27 ferramentas passivas (DNS, WHOIS, enumeração de subdomínios, Shodan, certificate transparency, DNS histórico, etc.). Se ele descobre IPs e o operador optou por scanning ativo, um portão de aprovação com humano no loop pausa a execução e exibe os alvos para revisão antes de prosseguir.
O port scanning tem 2 ferramentas (naabu, nmap). O vulnerability scanning tem 12 (Nuclei, DalFox, WPScan, detecção de WAF, análise TLS, etc.). O triage identifica lacunas na cobertura. O report sintetiza tudo em um documento Markdown estruturado.
A palavra-chave é autônomo: cada agente usa o padrão ReAct — Reason + Act — para escolher ferramentas, interpretar resultados e decidir os próximos passos. O orquestrador gerencia o fluxo de estado e o roteamento condicional, mas nunca diz a um agente qual ferramenta usar.
Por que agentes ReAct, não chains
Uma chain é uma sequência fixa: chame a ferramenta A, depois B, depois C. Um agente ReAct é um loop: o modelo observa o estado atual, raciocina sobre o que está faltando, escolhe uma ferramenta, observa o resultado e repete até decidir que terminou.
Para pentest isso importa porque a estratégia certa depende do que você encontra. Se o OSINT revela um site WordPress, o agente deve priorizar WPScan e enumeração de diretórios. Se encontra um endpoint de API, introspecção GraphQL se torna relevante. Se subdomínios apontam para IPs na nuvem, scanning de buckets S3 faz sentido. Codificar essas decisões é possível, mas frágil — cada nova forma de alvo requer nova lógica de ramificação.
Com agentes ReAct, o modelo lê um skill prompt (um documento markdown estilo playbook descrevendo a estratégia para aquela fase) e seleciona ferramentas autonomamente com base no que observa. A restrição chave é que o modelo só pode chamar ferramentas que são explicitamente fornecidas — ele não pode alucinar capacidades.
LLM-as-a-judge: roteamento adaptativo
Após cada fase, um avaliador de saída estruturada (o “juiz”) pontua a qualidade da fase em uma escala de 0.0 a 1.0 e recomenda o roteamento. Se o port scanning retornou resultados vazios, o juiz roteia diretamente para o triage em vez de desperdiçar tempo com vulnerability scanning. Se o OSINT não encontrou IPs, o pipeline pula o scanning ativo por completo.
Isso substitui o que normalmente seria uma floresta de blocos if/elif por uma única chamada de LLM que avalia o contexto de forma holística. O juiz tem seu próprio skill prompt que define critérios de pontuação e regras de roteamento.
Validação de entrada como preocupação de primeira classe
Toda ferramenta valida suas entradas através de guard_target(), uma camada de validação que classifica tipos de entrada (IP, domínio, URL, CIDR) e rejeita qualquer coisa que não corresponda ao tipo esperado pela ferramenta. Isso é imposto no nível de código — ele lança ToolException, não apenas instruções de prompt que o modelo pode ignorar.
Metacaracteres de shell, tentativas de path traversal e faixas de IP privadas são rejeitados antes de qualquer execução de comando. O modelo recebe um erro estruturado e pode tentar novamente com entrada corrigida.
Essa foi uma decisão de design inegociável. Quando um LLM decide quais comandos executar, a fronteira entre “saída do modelo” e “entrada do sistema” se torna sua superfície de ataque primária. Instruções em nível de prompt são necessárias, mas insuficientes — você precisa de imposição em nível de código.
Resiliência de ferramentas
Três mecanismos impedem que falhas de ferramentas se propaguem em cascata:
- ToolException + handle_tool_error: toda ferramenta propaga erros limpos de volta ao LLM como resultados normais de ferramenta, não como crashes. O modelo lê o erro e se adapta.
- Circuit breakers: ferramentas baseadas em HTTP (Shodan, VirusTotal, etc.) usam circuit breakers por serviço que desabilitam a ferramenta após falhas repetidas. Isso evita que o agente desperdice seu orçamento de iterações em um serviço que está fora do ar.
- Gating automático de provedores: ferramentas que requerem API keys não configuradas são removidas da lista de ferramentas do agente na inicialização. O LLM nunca vê ferramentas que não pode usar.
Configuração de modelo por agente
Diferentes fases têm diferentes requisitos. OSINT envolve muitas chamadas de ferramentas com raciocínio simples — um modelo rápido e barato funciona bem. Geração de relatórios requer sintetizar achados em prosa coerente — um modelo mais capaz ajuda.
O Fackel usa variáveis de ambiente (FACKEL_MODEL_OSINT, FACKEL_MODEL_REPORT, etc.) para que cada agente possa usar um modelo diferente. O padrão é gpt-5-mini para todos os agentes.
Prompting em duas camadas
Todos os agentes compartilham um soul prompt: um documento markdown que define identidade, regras anti-alucinação e restrições de saída. Cada agente também recebe um skill prompt: um playbook específico da fase com diretrizes de estratégia, padrões de uso de ferramentas e regras de priorização.
A separação importa porque previne drift de prompt. O soul prompt impõe comportamento consistente (nunca fabricar achados, sempre citar output de ferramenta) enquanto skill prompts podem ser iterados independentemente por fase.
Observabilidade
Definir duas variáveis de ambiente habilita tracing do LangSmith. Todas as fases dos agentes aparecem como traces hierárquicos com uso de tokens, I/O de ferramentas, latência e atividade de middleware. Nenhuma mudança de código é necessária — o sistema de callbacks do LangGraph cuida disso.
Para output no terminal, o Fackel transmite chamadas de ferramentas e resultados em tempo real. O modo verbose (-v) também mostra os passos de raciocínio do modelo (a parte “thought” do ReAct).
O que eu faria diferente
Schemas de saída mais estritos. Alguns agentes retornam resumos em texto livre que agentes downstream precisam parsear. Saída estruturada (modelos Pydantic) para comunicação entre fases tornaria o pipeline mais determinístico.
Rastreamento de custo por execução. O LangSmith fornece contagens de tokens, mas um estimador de custo dentro do pipeline que pudesse interromper a execução se uma rodada exceder um orçamento seria valioso para uso em produção.
Melhor cobertura de testes para decisões dos agentes. Testar unitariamente ferramentas individuais é direto. Testar se um agente toma decisões estratégicas razoáveis dado um determinado contexto é mais difícil e é onde está a maior parte do risco.
Executando
# Install
git clone https://github.com/flaviomilan/fackel.git
cd fackel && uv sync --python 3.12
# Configure
cp .env.example .env # set OPENAI_API_KEY
# Passive scan only
fackel example.com --no-active-scan
# Full scan with verbose output
fackel example.com -v
O projeto é open source sob Apache 2.0: github.com/flaviomilan/fackel.