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:
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:
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.
| Regra | Achado |
|---|---|
| 10038 | Content Security Policy (CSP) Header Not Set |
| 10049 | Non-Storable Content |
| 10063 | Deprecated Feature Policy Header Set |
| 10096 | Timestamp Disclosure - Unix |
| 10098 | Cross-Domain Misconfiguration |
| 10109 | Modern Web Application |
| 10110 | Dangerous JS Functions |
| 90004 | Cross-Origin-Embedder-Policy Header Missing or Invalid |
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.