DAST em Pipelines CI: OWASP ZAP, Nuclei e Testes Dinâmicos Automatizados: Do Básico ao Avançado Já leu

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

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&amp;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.

Referências


Artigos relacionados