Case detalhado 2024-11 → 2024-11

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.yml tem.

O resultado classificado, por grupo:

Grupo (anonimizado)ReposOK
Cliente A (gov)10042
Cliente B (produto)267
Squad interno1913
Cliente C (gov estadual)165
Cliente D (privado)1210
Cliente E (privado)43
Cliente F (privado)11
Total17881+ (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"
process-project.sh — 10 passos do rollout trecho ilustrativo, anonimizado; PAT real foi rotacionado e nunca aparece em snippet público

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.yml exige 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_TOKEN do ambiente. Conto aqui porque “consultor de segurança commitou token” é exatamente o tipo de erro que o próprio rollout deveria pegar.

Resultado

178em 8 grupos
Repos auditados
evidência
-93%~30min → ~2min
Tempo por repo
evidência
266pares new/original
Patches `.gitlab-ci.yml` gerados
evidência
87 / 6 / 91estado pós-rollout
Repos OK / Needs Fix corrigidos / Sem CI
evidência

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.

Resultados mensuráveis

178em 8 grupos
Repos auditados
evidência
-93%~30min → ~2min
Tempo por repo
evidência
266pares new/original
Patches `.gitlab-ci.yml` gerados
evidência
87 / 6 / 91OK / corrigidos / sem CI
Status do parque
evidência