O Que é DAST e Por Que Você Precisa Dele
DAST (Dynamic Application Security Testing) é uma metodologia de teste de segurança que analisa aplicações em tempo de execução, diferentemente do SAST (Static Application Security Testing) que examina o código-fonte sem rodá-lo. Imagine que SAST é como revisar um projeto de engenharia no papel, enquanto DAST é como testar o edifício já construído.
A razão pela qual DAST é crítico em pipelines CI/CD é simples: vulnerabilidades aparecem não apenas no código, mas na forma como a aplicação se comporta quando está rodando, interagindo com bancos de dados, APIs externas e com entrada de usuários reais. Um pipeline CI que não inclui DAST deixa brechas para ataques que ferramentas estáticas nunca encontrariam — como injeção SQL dinâmica, autenticação quebrada, ou lógica de negócio falha.
OWASP ZAP: Fundações e Integração em Pipelines
O Que é OWASP ZAP
O OWASP ZAP (Zed Attack Proxy) é um proxy de segurança de código aberto que intercepta requisições HTTP/HTTPS e as analisa contra vulnerabilidades conhecidas. Ele funciona como um "homem do meio" inteligente — capturando o tráfego da sua aplicação e testando-o contra milhares de assinaturas de ataque documentadas pela OWASP.
ZAP tem dois modos principais: exploração interativa (você usa manualmente) e automação via CLI, que é o que nos interessa para CI/CD. A grande vantagem é que ele entende contexto da aplicação — não é apenas um scanner burro que faz requisições aleatórias.
Configurando ZAP em um Pipeline GitHub Actions
Aqui está um exemplo real de integração com GitHub Actions. Este workflow baixa ZAP, roda uma varredura automática contra sua aplicação e gera um relatório:
name: DAST com OWASP ZAP
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
dast:
runs-on: ubuntu-latest
services:
app:
image: seu-app:latest
ports:
- 8080:8080
options: >-
--health-cmd "curl -f http://localhost:8080/health || exit 1"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: Aguardar aplicação inicializar
run: |
for i in {1..30}; do
curl -f http://localhost:8080/health && break
sleep 2
done
- name: Executar OWASP ZAP Baseline Scan
uses: zaproxy/action-baseline@v0.7.0
with:
target: 'http://localhost:8080'
rules_file_name: '.zap/rules.tsv'
cmd_options: '-a'
- name: Upload resultados ZAP
if: always()
uses: actions/upload-artifact@v3
with:
name: zap-results
path: report_html.html
- name: Falhar pipeline se vulnerabilidades críticas
if: failure()
run: exit 1
Personalizando Contexto e Escopo
O poder real do ZAP vem quando você o configura para entender sua aplicação. Um arquivo de contexto em XML permite que ZAP saiba quais URLs testar, quais ignorar e até mesmo credenciais de login:
<!-- .zap/context.xml -->
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<context>
<name>Minha Aplicação</name>
<desc></desc>
<excludePattern>.*\.(js|css|jpg|png|gif|pdf|zip)$</excludePattern>
<includePattern>.*localhost:8080.*</includePattern>
<authentication>
<type>2</type> <!-- 1=Manual, 2=Script, 3=Cookie -->
<loginUrl>http://localhost:8080/login</loginUrl>
<loginRequestData>username=admin&password=senha123</loginRequestData>
</authentication>
<users>
<user>
<name>Admin User</name>
<credentials>
<param name="username">admin</param>
<param name="password">senha123</param>
</credentials>
</user>
</users>
<scope>
<include>http://localhost:8080.*</include>
</scope>
</context>
Depois, você refere este contexto na execução do ZAP via CLI:
zaproxy -cmd \
-contextFile .zap/context.xml \
-runzaproxy -t http://localhost:8080 \
-f html \
-r report.html
Nuclei: DAST Moderno e Focado em Velocidade
Por Que Nuclei é Diferente
Enquanto OWASP ZAP é um proxy tradicional que intercepta tráfego, Nuclei é um engine de scanning moderno baseado em templates YAML. Ele foi feito para segurança em escala — você pode testar centenas de endpoints em paralelo, e cada teste é descrito em um arquivo YAML legível e versionável.
Nuclei é mais "programável" que ZAP: em vez de depender de regras internas, você define exatamente qual request fazer, como processar a resposta e o que considerar uma vulnerabilidade. Isso o torna ideal para testes customizados e para encontrar vulnerabilidades específicas do seu domínio.
Instalação e Primeiro Scan
Nuclei roda em qualquer sistema com Go instalado:
# Instalação via Go
go install -v github.com/projectdiscovery/nuclei/v2/cmd/nuclei@latest
# Atualizar templates (base de conhecimento de vulnerabilidades)
nuclei -update-templates
# Primeiro scan básico
nuclei -u http://localhost:8080 -o resultados.txt
Templates Nuclei: Entendendo a Lógica
Templates no Nuclei são YAML com estrutura bem definida. Aqui está um exemplo real que detecta um header de segurança faltante:
# templates/security/missing-security-headers.yaml
id: missing-security-headers
info:
name: Missing Security Headers
author: seu-nome
severity: medium
reference: https://owasp.org/www-project-secure-headers/
tags: security,headers
requests:
- method: GET
path:
- "/"
matchers:
- type: dsl
dsl:
- 'len(header_namemap("Strict-Transport-Security")) == 0'
- 'len(header_namemap("X-Content-Type-Options")) == 0'
condition: or
extractors:
- type: dsl
dsl:
- header_namemap("Strict-Transport-Security")
- header_namemap("X-Content-Type-Options")
Este template faz uma requisição GET para /, verifica se headers críticos de segurança estão ausentes e extrai seus valores se existirem. O condition: or significa que a vulnerabilidade é encontrada se qualquer um dos headers estiver faltando.
Template Customizado: Detecção de Injeção SQL
Aqui está um template mais sofisticado que testa um parâmetro específico contra injeção SQL:
# templates/injection/sql-injection-param.yaml
id: sql-injection-custom
info:
name: SQL Injection in Search Parameter
author: seu-nome
severity: critical
tags: sqli,injection
requests:
- method: GET
path:
- "/search?q=test' OR '1'='1"
stop-at-first-match: true
matchers:
- type: word
part: body
words:
- "sql syntax"
- "database error"
- "mysql_fetch"
- "ora-"
case-insensitive: true
- type: regex
part: response_header
regex:
- "(?i)(SQL|MySQL|Oracle|PostgreSQL)"
Este template injeta uma carga SQL clássica (' OR '1'='1) e procura por palavras-chave que indicam erro de banco de dados. Se encontrar, marca como SQL injection crítico.
Rodando Nuclei em Seu Pipeline CI
# .github/workflows/nuclei-dast.yml
name: DAST com Nuclei
on:
push:
branches: [main, develop]
jobs:
nuclei:
runs-on: ubuntu-latest
services:
app:
image: seu-app:latest
ports:
- 8080:8080
steps:
- uses: actions/checkout@v3
- name: Instalar Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Instalar Nuclei
run: go install -v github.com/projectdiscovery/nuclei/v2/cmd/nuclei@latest
- name: Atualizar templates
run: nuclei -update-templates
- name: Executar Nuclei Scan
run: |
nuclei -u http://localhost:8080 \
-t ./templates/ \
-severity critical,high \
-o nuclei-results.txt
- name: Upload resultados
if: always()
uses: actions/upload-artifact@v3
with:
name: nuclei-results
path: nuclei-results.txt
- name: Falhar se crítica encontrada
if: always()
run: |
if grep -q "critical" nuclei-results.txt; then
echo "Vulnerabilidades críticas encontradas!"
exit 1
fi
Testes Dinâmicos Automatizados: Orquestração e Melhores Práticas
Arquitetura de um Pipeline DAST Completo
Um pipeline robusto não roda apenas uma ferramenta — ele orquestra múltiplas validações, cada uma focada em um aspecto da segurança. Aqui está uma arquitetura real:
┌─────────────────────────────────────────┐
│ 1. Build & Deploy │
│ Docker build → Iniciar serviços │
└─────────────────┬───────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 2. Health Checks │
│ Aguardar aplicação estar pronta │
└─────────────────┬───────────────────────┘
│
┌─────────┴─────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ 3a. OWASP │ │ 3b. Nuclei │
│ ZAP │ │ Templates │
│ (Baseline) │ │ (Custom) │
└──────────────┘ └──────────────┘
│ │
└─────────┬─────────┘
▼
┌─────────────────────────────────────────┐
│ 4. Consolidar Resultados │
│ Parse reports, deduplica achados │
└─────────────────┬───────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 5. Decidir: Pass/Fail │
│ Se crítica/alta → Falha pipeline │
└─────────────────────────────────────────┘
Implementação em GitLab CI
GitLab CI oferece recursos nativos para DAST. Aqui está um exemplo completo com dois jobs paralelos:
# .gitlab-ci.yml
stages:
- build
- test
- dast
- report
variables:
DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
build:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker build -t $DOCKER_IMAGE .
- docker push $DOCKER_IMAGE
dast_zap:
stage: dast
image: owasp/zap2docker-stable:latest
services:
- name: $DOCKER_IMAGE
alias: app
variables:
TARGET_URL: "http://app:8080"
script:
- |
zaproxy -cmd \
-autorun /zap/rules.tsv \
-t $TARGET_URL \
-f html \
-r report-zap.html
artifacts:
paths:
- report-zap.html
expire_in: 30 days
allow_failure: true
dast_nuclei:
stage: dast
image: projectdiscovery/nuclei:latest
services:
- name: $DOCKER_IMAGE
alias: app
script:
- |
nuclei -u http://app:8080 \
-t /root/nuclei-templates/ \
-severity critical,high \
-o nuclei-results.txt
artifacts:
paths:
- nuclei-results.txt
expire_in: 30 days
allow_failure: true
report:
stage: report
image: alpine:latest
script:
- |
echo "=== DAST Results Summary ==="
echo "OWASP ZAP Results:"
[ -f report-zap.html ] && echo "✓ Report disponível" || echo "✗ Não gerado"
echo ""
echo "Nuclei Results:"
if [ -f nuclei-results.txt ]; then
wc -l nuclei-results.txt
grep -c "critical" nuclei-results.txt || echo "Sem críticas"
fi
dependencies:
- dast_zap
- dast_nuclei
Tratamento de Falsos Positivos
DAST gera falsos positivos naturalmente. Um scanner não entende contexto de negócio. Por isso, você precisa de um mecanismo de ignorar achados válidos:
# scripts/filter_dast_results.py
import json
import sys
WHITELIST = [
# Formato: {id, endpoint, tipo}
{"id": "10038", "path": "/admin", "reason": "X-Frame-Options exigido para admin apenas"},
{"id": "20014", "path": "/api/public", "reason": "Cookie sem SameSite em endpoint público é aceitável"},
]
def load_nuclei_results(file_path):
"""Parse resultados Nuclei em JSON"""
with open(file_path, 'r') as f:
return [json.loads(line) for line in f if line.strip()]
def is_whitelisted(result):
"""Verifica se achado está na whitelist"""
for item in WHITELIST:
if (result.get('template_id') == item['id'] and
item['path'] in result.get('host', '')):
return True
return False
def filter_results(input_file, output_file):
"""Remove achados whitelistados"""
results = load_nuclei_results(input_file)
filtered = [r for r in results if not is_whitelisted(r)]
critical_count = sum(1 for r in filtered if r.get('info', {}).get('severity') == 'critical')
with open(output_file, 'w') as f:
for r in filtered:
f.write(json.dumps(r) + '\n')
print(f"Original: {len(results)} | Filtrados: {len(filtered)} | Críticos: {critical_count}")
return critical_count
if __name__ == "__main__":
critical_count = filter_results(sys.argv[1], sys.argv[2])
sys.exit(1 if critical_count > 0 else 0)
Integre no pipeline:
# Após nuclei executar
python scripts/filter_dast_results.py nuclei-results.txt nuclei-filtered.txt
exit_code=$?
if [ $exit_code -ne 0 ]; then
echo "Achados críticos após filtro!"
exit 1
fi
Comparando Resultados Entre Execuções
Para evitar regressões, compare o relatório atual com a baseline anterior:
# scripts/compare_dast_baseline.py
import json
from collections import Counter
def load_results(file_path):
"""Carrega resultados em JSON"""
try:
with open(file_path, 'r') as f:
return [json.loads(line) for line in f if line.strip()]
except FileNotFoundError:
return []
def normalize(result):
"""Cria chave única para dedupliação"""
return f"{result.get('template_id')}#{result.get('host')}"
def compare(current_file, baseline_file):
"""Compara achados atuais com baseline"""
current = load_results(current_file)
baseline = load_results(baseline_file)
current_keys = Counter(normalize(r) for r in current)
baseline_keys = Counter(normalize(r) for r in baseline)
# Novos achados
new_vulns = set(current_keys.keys()) - set(baseline_keys.keys())
# Achados resolvidos
fixed_vulns = set(baseline_keys.keys()) - set(current_keys.keys())
# Piora (mais de uma mesma vulnerabilidade)
worse = {k: (baseline_keys[k], current_keys[k]) for k in current_keys
if current_keys[k] > baseline_keys.get(k, 0)}
print(f"Novos achados: {len(new_vulns)}")
print(f"Achados resolvidos: {len(fixed_vulns)}")
print(f"Piora (mais instâncias): {len(worse)}")
if len(new_vulns) > 0:
print("\n⚠️ NOVOS ACHADOS:")
for vuln in list(new_vulns)[:5]:
print(f" - {vuln}")
return len(new_vulns) == 0 and len(worse) == 0
if __name__ == "__main__":
import sys
success = compare(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else 'baseline.json')
sys.exit(0 if success else 1)
Conclusão
Você aprendeu que DAST é essencial em pipelines CI porque testa a aplicação viva, encontrando vulnerabilidades que SAST nunca encontraria. OWASP ZAP é a ferramenta clássica para quem quer um proxy inteligente com autenticação e contexto profundo, enquanto Nuclei oferece velocidade, escalabilidade e templates versionáveis para testes customizados.
A lição crítica é que automatizar DAST é apenas o começo — você precisa lidar com falsos positivos através de whitelists, comparar resultados contra baselines para detectar regressões, e orquestrar múltiplas ferramentas em paralelo. Um pipeline DAST maduro não falha no primeiro "erro" que encontra; ele entende o contexto do seu negócio e decide inteligentemente quando bloquear a release.