SAST em escala: rollout de pipeline de segurança em 178 repos GitLab
Rollout SAST em 178 repos GitLab: ~30min→2min/repo via 30 scripts, 266 patches `.gitlab-ci.yml` em 4 dias, 87 OK / 6 corrigidos / 91 sem CI mapeados.
DevOps Engineer (Estagiário) · Consultoria AWS Select Partner
- GitLab CI
- Semgrep
- SonarQube
- Bash
- Python
- GitLab API
TL;DR
- 178 projetos em 8 grupos GitLab auditados e classificados em 4 dias corridos (25–28/11/2024).
- Tempo por repo caiu de ~30min para ~2min depois que o rollout virou script — ganho de ~93%.
- 266 patches
.gitlab-ci.yml(pares new/original) gerados, mais 23 CSVs de análise por lote e 30 scripts (Bash + Python) versionados.
Contexto
A Consultoria AWS Select Partner mantinha 178 projetos distribuídos em 8 grupos GitLab — clientes diferentes (públicos e privados), squads diferentes, e padrões de CI heterogêneos ou inexistentes. SAST (análise estática de segurança) existia em alguns repos, faltava em outros, e ninguém tinha um dashboard único de “quem está coberto e quem não”.
O cenário típico: vulnerabilidades só apareciam em revisão manual de MR ou — pior — em homologação. Não era falha de ferramenta, era falta de base instalada. O template oficial de SAST do GitLab (que orquestra Semgrep sob o capô para a maioria das stacks suportadas) e a instância interna de SonarQube já existiam; o gap era de adoção uniforme.
Aplicar o template à mão repo-a-repo levava ~30 minutos (clone, criar branch, editar .gitlab-ci.yml, validar localmente, abrir MR, copiar URL pra planilha de tracking). Multiplicado por 178, o rollout manual projetava >85 horas só de toque humano — fora janela de revisão de cada squad.
Ação
A entrega foi estruturada em duas fases sequenciais, cada uma com seu próprio script base e seus próprios artefatos versionados.
Auditoria do parque
Primeiro pass via GitLab API: para cada um dos 8 grupos, listar projetos, baixar .gitlab-ci.yml (quando existia), e classificar em três baldes:
- OK — repo já roda SAST corretamente.
- Needs Fix — repo tem
.gitlab-ci.yml, mas sem job SAST ou com configuração quebrada. - Sem CI — repo nem
.gitlab-ci.ymltem.
O resultado classificado, por grupo:
| Grupo (anonimizado) | Repos | OK |
|---|---|---|
| Cliente A (gov) | 100 | 42 |
| Cliente B (produto) | 26 | 7 |
| Squad interno | 19 | 13 |
| Cliente C (gov estadual) | 16 | 5 |
| Cliente D (privado) | 12 | 10 |
| Cliente E (privado) | 4 | 3 |
| Cliente F (privado) | 1 | 1 |
| Total | 178 | 81+ (87 com OK marginais) |
Os 23 CSVs gerados — um por lote de varredura, mais 11 logs paralelos — viraram a fonte única de verdade consultável depois da execução. Ninguém precisava re-rodar a auditoria pra saber em que estado um repo estava em 28/11.
Automação do rollout
A segunda fase foi o script process-project.sh (129 linhas Bash, com cores ANSI, error handling, validação yamllint). 10 passos automatizados por repo:
#!/usr/bin/env bash
# process-project.sh — trecho ilustrativo, anonimizado
set -euo pipefail
PROJECT="$1" # ex: cliente-a/api-foo
TOKEN="${GITLAB_TOKEN:?GITLAB_TOKEN não definido}"
TEMPLATE_PATCH="patches/sast-template.yml"
# 1. clone
git clone "https://oauth2:${TOKEN}@gitlab.example.tld/${PROJECT}.git" repo
cd repo
# 2-3. fallback develop -> main; cria branch dedicada
BASE_BRANCH=$(git rev-parse --verify origin/develop &>/dev/null \
&& echo develop || echo main)
git checkout -b chore/enable-sast "origin/${BASE_BRANCH}"
# 4-5. aplica patch + valida YAML
python scripts/merge-ci.py .gitlab-ci.yml "${TEMPLATE_PATCH}"
yamllint .gitlab-ci.yml
# 6. diff humanamente legível salvo no log do lote
git diff --no-color > "../logs/${PROJECT//\//_}.diff"
# 7-8. commit + push
git add .gitlab-ci.yml
git commit -m "chore(ci): habilita template SAST padronizado"
git push origin chore/enable-sast
# 9-10. abre MR via API e captura URL pra planilha de tracking
MR_URL=$(curl -sf --header "PRIVATE-TOKEN: ${TOKEN}" \
--data "source_branch=chore/enable-sast&target_branch=${BASE_BRANCH}&title=chore(ci): habilita SAST" \
"https://gitlab.example.tld/api/v4/projects/${PROJECT//\//%2F}/merge_requests" \
| python -c "import sys,json; print(json.load(sys.stdin)['web_url'])")
echo "${PROJECT},${MR_URL}" >> "../logs/mr-tracking.csv"
O ganho por repo virou ~2 minutos (clone + push + MR). Ainda exige humano pra fazer review final e merge — o script não auto-merge, por design — mas o tempo de toque caiu ~93%.
Em paralelo, 30 scripts foram criados/iterados (Bash + Python, várias versões v2–v6), cobrindo: parsing de listas de projetos, deduplicação de patches, geração dos pares new/original, e relatórios consolidados.
Trade-offs e o que NÃO fiz
- GitLab SAST template + Semgrep em vez de Semgrep standalone: o template oficial já cobre as linguagens da maioria dos squads e roda Semgrep internamente, então padronizar via template significa um lugar único pra atualizar regras em vez de manter configuração Semgrep custom em cada repo. Trade-off: regras menos finas no curto prazo, governança melhor no longo.
- SonarQube manteve papel separado: o SonarQube interno continuou sendo a fonte de qualidade de código (smells, dívida técnica, cobertura). SAST do GitLab cobre segurança. Não fundi os dois — cada um fala uma língua diferente para um dashboard diferente.
- Não rodei auto-merge: os 266 patches abriram MR; quem aprova é o squad dono. Aceitar 1 patch errado em 178 repos é trivial; em 178 sem revisão humana, vira incidente. A automação parou no MR aberto, de propósito.
- Não medi vulnerabilidades mitigadas pós-rollout: a entrega cobriu adoção (quantos repos passaram a rodar SAST), não eficácia (quantas issues foram corrigidas no primeiro mês). Isso é trabalho do squad de segurança no ciclo seguinte e foi declarado como gap honesto ao líder técnico.
- Não ataquei os 91 “sem CI” no mesmo rollout: repo sem
.gitlab-ci.ymlexige conversa com squad sobre runner, ambiente, segredos, build target — não dá pra empurrar template SAST num projeto que nunca rodou pipeline. Ficaram listados, com responsável atribuído, fora do escopo desses 4 dias. - Credenciais hardcoded foram bug descoberto no próprio script: durante revisão pós-execução achei PAT GitLab embutido em duas linhas de uma versão antiga do script. Foi rotacionado imediatamente e o script reescrito pra ler
GITLAB_TOKENdo ambiente. Conto aqui porque “consultor de segurança commitou token” é exatamente o tipo de erro que o próprio rollout deveria pegar.
Resultado
Em ~4 dias corridos (25–28/11/2024), o parque saiu de estado opaco (“não sabemos quem roda SAST”) para estado mapeado e patcheável: 87 repos confirmados OK, 6 corrigidos via patch direto na fase de rollout, e 91 priorizados como backlog explícito de “habilitar CI antes de habilitar SAST”. Os 266 patches viraram histórico auditável — qualquer squad podia abrir o .diff do próprio repo e ver exatamente o que ia mudar antes de aprovar o MR.
Sendo honesto sobre o que não está nesta página: a métrica de vulnerabilidades efetivamente mitigadas (high/critical por categoria, por repo) ficou para o ciclo seguinte do squad de segurança. O que esta entrega prova é adoção em escala, não eficácia da regra — e os dois são problemas diferentes, com donos diferentes.
O que fica publicável: a base instalada de SAST padronizado em centenas de repos, o script reusável, e o relatório executivo (FULL_ROLLOUT_REPORT.md, 16KB) que sobreviveu à minha saída do squad — alguém pode rodar o mesmo rollout em outros 100 projetos amanhã sem precisar reinventar o pipeline.