Três Portões

capítulo 03

Gates em Ação

Cada bloqueio abaixo é real, capturado direto do pipeline rodando no repositório público. Nenhuma captura de tela aqui é ilustrativa.

Gate 1 — Gitleaks

Um PR de teste (#1) adicionou um arquivo com uma credencial AWS falsa, no formato reconhecido pela regra aws-access-token. A primeira tentativa usou o exemplo mais famoso da internet — AKIAIOSFODNN7EXAMPLE — e passou batido: essa string está no allowlist interno do próprio Gitleaks, por aparecer em milhares de tutoriais oficiais da AWS. Trocando por uma chave fake diferente, o bloqueio aconteceu de verdade:

Check do GitHub Actions mostrando o Gitleaks bloqueando o PR, com a regra aws-access-token detectada no arquivo secret-test.txt
gitleaks-action falhando o check do PR #1 — regra aws-access-token, arquivo secret-test.txt
o mesmo PR, dois gates, dois resultados

No PR #2 (o do gate 2, abaixo), uma senha hardcoded "supersecret123" passou limpo pelo Gitleaks — baixa entropia, nenhum padrão de provedor conhecido — mas foi pega pelo SonarCloud. É a prova prática de que os dois gates cobrem coisas diferentes, não a mesma coisa duas vezes.

Gate 2 — SonarCloud

O PR #2 introduziu um arquivo login.js com problemas clássicos, de propósito:

const DB_PASSWORD = "supersecret123";

function buildQuery(userId) {
  return "SELECT * FROM users WHERE id = " + userId;
}

function runCommand(input) {
  return eval(input);
}

Credencial hardcoded, SQL montado por concatenação, eval() de entrada não validada, e nenhum teste cobrindo essas linhas. O Quality Gate falhou:

Comentário do SonarCloud no PR mostrando Quality Gate failed, com a condição Security Rating on New Code = D, exigido A ou melhor
Quality Gate failed — Security Rating on New Code = D (exigido ≥ A)

Gate 3 — OWASP ZAP

O workflow pre-deploy-dast.yml sobe o Juice Shop num container do próprio runner do Actions, espera ele responder, e roda o baseline scan do ZAP contra http://localhost:3000:

- name: Start Juice Shop (staging surrogate)
  run: docker run -d --name juice-shop -p 3000:3000 bkimminich/juice-shop

- name: ZAP Baseline Scan
  uses: zaproxy/action-baseline@v0.15.0
  with:
    target: 'http://localhost:3000'
    rules_file_name: 'zap/zap-baseline-rules.conf'
    fail_action: true

Resultado real, contra o alvo de verdade: 59 regras passaram, 8 tiveram achado real.

RegraAchado
10038Content Security Policy (CSP) Header Not Set
10049Non-Storable Content
10063Deprecated Feature Policy Header Set
10096Timestamp Disclosure - Unix
10098Cross-Domain Misconfiguration
10109Modern Web Application
10110Dangerous JS Functions
90004Cross-Origin-Embedder-Policy Header Missing or Invalid
Job do GitHub Actions ZAP Baseline Scan falhando após detectar os achados acima no Juice Shop
ZAP Baseline Scan falhando (exit code 2) contra o Juice Shop rodando no runner

Validando pelo terminal, sem abrir o navegador

Todo resultado acima também pode ser conferido direto da linha de comando, sem depender da interface do GitHub:

RUN_ID=$(gh run list --workflow=pre-deploy-dast.yml --limit 1 --json databaseId --jq '.[0].databaseId')
gh run watch $RUN_ID   # necessário: um run em andamento ainda não tem log disponível
JOB_ID=$(gh run view $RUN_ID --json jobs --jq '.jobs[0].databaseId')
gh run view $RUN_ID --log --job=$JOB_ID | grep -iE "WARN-NEW|FAIL-NEW|PASS:|Total of"

Cada peça resolve um problema específico: gh run list --json databaseId --jq pega o ID numérico do run sem abrir navegador; gh run watch é necessário porque pedir o log de um job ainda em andamento só devolve "logs will be available when it is complete"; gh run view --json jobs resolve o ID do job específico dentro do run (um run pode ter mais de um job); e o grep -iE final isola só as linhas de resumo do ZAP em meio ao log verboso completo.