11 - DevOps¶
Esta seção descreve, de forma fiel ao código, como o Quorum (quorum-sec-scan, v0.2.3) é construído, testado, versionado e distribuído. O Quorum é uma ferramenta CLI/Docker de consensus security scanning escrita em Go 1.26; não há serviço em execução contínua, frontend web, banco de dados ou API REST. Por isso, "DevOps" aqui significa essencialmente engenharia de release: um pipeline de Integração Contínua (CI) que prova que o consenso entre scanners realmente acontece, e uma Entrega Contínua (CD) que publica imagens Docker assinadas e binários nativos assinados, ambos com atestação de proveniência SLSA verificada no próprio release. O artefato é imutável e versionado; "deploy" é publicar; "rollback" é re-apontar uma tag móvel para um digest anterior.
Todos os fatos abaixo derivam diretamente de:
.github/workflows/ci.yml— jobbuild-test..github/workflows/e2e.yml— jobconsensus..github/workflows/release.yml— jobsimagesebinaries..goreleaser.yaml— build de binários nativos.action.yml— GitHub Action composite que envolve a imagem:full.
1. Visão geral¶
flowchart LR
dev[Desenvolvedor] -->|branch de feature| pr[Pull Request]
pr -->|on: pull_request| ci[ci.yml<br/>build-test]
pr -->|on: pull_request| e2e[e2e.yml<br/>consensus]
ci -->|verde| merge{merge em main}
e2e -->|verde| merge
merge -->|push: main| ci2[ci.yml + e2e.yml<br/>re-rodam em main]
merge -->|tag semver vX.Y.Z| rel[release.yml]
rel --> ghcr[(GHCR<br/>imagens :full/:slim)]
rel --> ghrel[(GitHub Release<br/>binarios + checksums)]
rel --> sig[cosign keyless + SLSA provenance]
| Estágio | Workflow | Gatilho | O que prova / faz |
|---|---|---|---|
| Build & teste | ci.yml |
push em main, qualquer pull_request |
Compila, go vet, go test -race, build do binário, smoke (list-scanners) |
| Prova de consenso | e2e.yml |
push em main, qualquer pull_request, workflow_dispatch |
Roda scanners reais sobre alvos conhecidos e falha se nenhum finding for corroborado por dois engines |
| Release | release.yml |
push de tag v[0-9]+.[0-9]+.[0-9]+, workflow_dispatch |
Publica imagens e binários, assina (cosign keyless), atesta e verifica proveniência SLSA |
2. Integração Contínua (CI)¶
2.1 ci.yml — job build-test¶
Gatilho: push para main e qualquer pull_request. Runner: ubuntu-latest. Go 1.26 com cache habilitado.
Passos exatos:
| # | Passo | Comando | Falha se |
|---|---|---|---|
| 1 | Checkout | actions/checkout@v4 |
— |
| 2 | Setup Go | actions/setup-go@v5 (go-version: "1.26", cache: true) |
— |
| 3 | Vet | go vet ./... |
Erro estático/suspeito reportado pelo vet |
| 4 | Test | go test -race ./... |
Qualquer teste falha ou data race detectado |
| 5 | Build | go build -trimpath -o dist/quorum ./cmd/quorum |
Falha de compilação |
| 6 | Smoke | ./dist/quorum list-scanners |
Binário não executa o comando básico |
Pontos relevantes:
-raceé obrigatório. O orquestrador faz fan-out paralelo com goroutines (um scanner por goroutine), portanto o detector de corridas é a primeira linha de defesa contra regressões de concorrência.- Os contract tests de cada adapter (Trivy, Grype, Checkov, KICS, Dockle, Kubescape) rodam contra fixtures em
internal/adapter/testdatadentro dogo test ./...— não exigem os scanners instalados, sendo determinísticos e rápidos. - O passo Smoke garante que o binário sobe e que o registro de scanners (
list-scanners) responde, sem depender de rede ou de scanners externos.
2.2 e2e.yml — job consensus¶
Este workflow é a prova viva do princípio do produto: "0 findings is not proof of safety" e o consenso só vale com scanners de verdade, não fixtures. Gatilho: push para main, qualquer pull_request e workflow_dispatch (execução manual).
Sequência:
- Checkout + Setup Go 1.26.
- Build do quorum para
dist/quorume adição dedist/e~/.local/binaoGITHUB_PATH. - Instalação dos scanners por download direto de releases (versões fixadas):
- Trivy
0.71.2, Grype0.114.0(comentário no workflow alerta que schemas antigos do DB de vulnerabilidades do Grype são aposentados pela Anchore), Checkov viapipx install checkov. - Verificação dos scanners e pré-busca do DB:
trivy --version,grype version,checkov --version,grype db update(falha cedo se o DB não puder ser baixado),docker pull alpine:3.10(garante que ambos os engines resolvem a mesma imagem local) equorum list-scanners. - Consenso de IaC (Trivy + Checkov sobre
examples/terraform,--type repo,--offline): exigesummary.multiDetected >= 1, senão emite::error::no cross-engine IaC consensus reachede falha. - Consenso de SCA (Trivy + Grype sobre
alpine:3.10,--type image,--offline): mesma regra demultiDetected >= 1. - Upload de relatórios (
iac.json,sca.json) como artefato, comif: always().
O
--offlinedesliga as consultas de alias na OSV.dev, tornando o e2e determinístico e independente de variações de rede em relação à correlação por alias.
flowchart TD
a[Build quorum] --> b[Instala trivy/grype/checkov<br/>versoes fixadas]
b --> c[grype db update + docker pull alpine:3.10]
c --> d[scan IaC: trivy+checkov]
c --> e[scan SCA: trivy+grype]
d --> f{multiDetected >= 1?}
e --> g{multiDetected >= 1?}
f -->|nao| x1[::error:: sem consenso IaC -> FALHA]
g -->|nao| x2[::error:: sem consenso SCA -> FALHA]
f -->|sim| h[upload iac.json/sca.json]
g -->|sim| h
3. Git flow e estratégia de branch¶
O fluxo é o GitHub Flow clássico, baseado em PR para main, com versionamento por tag semver.
gitGraph
commit id: "main"
branch feature/xyz
commit id: "trabalho"
commit id: "mais trabalho"
checkout main
merge feature/xyz tag: "PR + CI verde"
commit id: "v0.2.3" tag: "v0.2.3"
Regras práticas:
mainé a linha de release. Todo trabalho acontece em branches de feature/fix (ex.:fix/probe-timeout-...,ci/restrict-release-trigger-..., como visto no histórico recente).- Toda mudança entra via Pull Request. O PR aciona
ci.yml(build-test) ee2e.yml(consensus). Ambos devem estar verdes antes do merge. - Merge em
mainre-rodaci.ymlee2e.ymlnopushparamain(defesa em profundidade contra race de merge). - Release por tag semver. Apenas tags no formato
v[0-9]+.[0-9]+.[0-9]+(ex.:v0.2.3) disparamrelease.yml. Não há release automático por merge — a tag é o gesto explícito de publicação. - Tag móvel
v0(major) existe apenas para fixar o GitHub Action: consumidores usamuses: Martinez1991/quorum-sec-scan@v0. Essa tag move-se entre releases compatíveis. Orelease.ymlignora propositalmente tags comov0no gatilho (o padrão semver exigeX.Y.Z), evitando que mover o ponteiro do Action reconstrua imagens.
Checklist de PR (recomendado)¶
- [ ] Branch criada a partir de
mainatualizada. - [ ]
go vet ./...ego test -race ./...passam localmente. - [ ] Novos parsers de adapter têm contract test contra fixture em
internal/adapter/testdata. - [ ] CI (
build-test) verde. - [ ] E2E (
consensus) verde — o consenso real ainda ocorre. - [ ] Mensagens de commit seguem prefixos convencionais (
fix:,feat:,ci:, etc.);docs:/test:/chore:são filtrados do changelog (ver.goreleaser.yaml).
4. Entrega Contínua (CD) — release.yml¶
Gatilho de release: push de tag v[0-9]+.[0-9]+.[0-9]+ ou workflow_dispatch (com input version, default dev).
Permissões do workflow (mínimo necessário):
| Permissão | Por quê |
|---|---|
contents: read (job images) / contents: write (job binaries) |
Ler o repo; criar release e subir assets de binários |
packages: write |
Push das imagens no GHCR |
id-token: write |
OIDC para assinatura keyless com cosign (Sigstore) |
attestations: write |
Atestação de build-provenance SLSA |
4.1 Job images¶
Matriz de variantes (fail-fast: false):
| Variante | Dockerfile | Plataformas | Conteúdo |
|---|---|---|---|
full |
Dockerfile.full |
linux/amd64 |
Todos os scanners empacotados (binários dos scanners são amd64; grype DB pré-cacheado) |
slim |
Dockerfile |
linux/amd64, linux/arm64 |
Apenas o orquestrador |
Resolução de tags (passo meta), com image=ghcr.io/<owner>/<repo> em minúsculas e version derivada de GITHUB_REF_NAME sem o prefixo v:
| Variante | Tags publicadas |
|---|---|
full |
:full, :<version>, :<version>-full, :latest |
slim |
:slim, :<version>-slim |
Pipeline do job:
- Checkout.
- Resolve versão e nome da imagem (passo
meta, escreveimage,version,tagsemGITHUB_OUTPUT). - QEMU + Buildx (necessários para build multi-arch do
slim). - Login no GHCR com
GITHUB_TOKEN. - Install cosign (
sigstore/cosign-installer@v3). - Build & push via
docker/build-push-action@v6comprovenance: true,sbom: true, cache GHA por escopo (quorum-<variant>) ebuild-args: VERSION=<version>. - Sign image (keyless):
cosign sign --yes "${IMAGE}@${DIGEST}"— assina o digest do manifesto (multi-arch) uma vez, cobrindo todas as tags que apontam para ele. Identidade vem do token OIDC do GitHub. - Attest SLSA build provenance:
actions/attest-build-provenance@v2gera atestação SLSA v1 (subject-digest= digest da imagem,push-to-registry: true). - Verify provenance attestation:
gh attestation verify "oci://${IMAGE}@${DIGEST}" --repo ...— re-verifica fim-a-fim (log de transparência Sigstore + identidade OIDC). Atestação quebrada falha o release. - Summary: escreve variante, plataformas, digest e tags no
GITHUB_STEP_SUMMARY.
4.2 Job binaries¶
Condição: if: github.ref_type == 'tag' (GoReleaser precisa da tag). Permissões adicionais: contents: write, id-token: write, attestations: write.
Pipeline:
- Checkout com
fetch-depth: 0(GoReleaser precisa do histórico completo para o changelog). - Setup Go 1.26.
- Install cosign.
- GoReleaser (
goreleaser/goreleaser-action@v6,version: "~> v2",args: release --clean) — conforme.goreleaser.yaml: - Builds
CGO_ENABLED=0,-trimpath,ldflags -s -w -X main.version={{.Version}}. - Matriz
goos: [linux, darwin, windows]xgoarch: [amd64, arm64]. - Archives
quorum_<version>_<os>_<arch>(zip no Windows) incluindoREADME.md,README.pt-BR.md,LICENSEe o diretóriocrosswalk/**. checksums.txtcobrindo todos os artefatos.- Assinatura keyless do arquivo de checksums via
cosign sign-blob(gera${artifact}.sige${artifact}.pem). Como o checksum fixa os hashes de todos os artefatos, a assinatura sobre ele cobre o release inteiro. - Cria o GitHub Release (
draft: false,prerelease: auto) com changelog do GitHub (excluidocs:/test:/chore:). - Attest SLSA build provenance for binaries:
subject-checksums: dist/checksums.txt— uma atestação cobrindo todos os artefatos listados. - Verify binary provenance attestation:
gh attestation verify dist/quorum_*_linux_amd64.tar.gz --repo ...(spot-check de um artefato).
flowchart TD
tag[push tag vX.Y.Z] --> imgs[job: images]
tag --> bins[job: binaries]
subgraph images[job images — matriz full/slim]
m1[meta: resolve tags] --> m2[buildx + qemu]
m2 --> m3[build-push provenance+sbom]
m3 --> m4[cosign sign digest]
m4 --> m5[attest SLSA provenance]
m5 --> m6[gh attestation verify]
end
subgraph binaries[job binaries — GoReleaser]
b1[goreleaser release --clean] --> b2[archives + checksums.txt]
b2 --> b3[cosign sign-blob checksums]
b3 --> b4[GitHub Release]
b4 --> b5[attest SLSA por checksums]
b5 --> b6[gh attestation verify spot-check]
end
m6 --> ok1[(GHCR: imagens assinadas + atestadas)]
b6 --> ok2[(GitHub Release: binarios assinados + atestados)]
5. "Deploy" para um CLI/Docker¶
Não há ambiente de runtime gerenciado por este projeto. Deploy = publicar artefatos imutáveis e verificáveis. A unidade de implantação é o usuário ou pipeline consumidor que puxa a imagem ou baixa o binário.
| Conceito tradicional | Equivalente no Quorum |
|---|---|
| Servidor/ambiente de runtime | N/A — não há serviço persistente |
| Deploy | Publicar imagens no GHCR + Release de binários no GitHub |
| Versão imutável | Digest da imagem (@sha256:...) e tag semver |
| Integridade/origem | cosign keyless (OIDC) + atestação de proveniência SLSA |
| Configuração de runtime | Flags da CLI (--type, --scanners, --fail-on, ...) e mounts Docker |
Como o consumidor verifica antes de usar¶
Imagem:
cosign verify ghcr.io/martinez1991/quorum-sec-scan:slim \
--certificate-identity-regexp \
"https://github.com/Martinez1991/quorum-sec-scan/.github/workflows/release.yml@.*" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
gh attestation verify oci://ghcr.io/martinez1991/quorum-sec-scan:slim \
--repo Martinez1991/quorum-sec-scan
A própria action.yml faz isso automaticamente: por padrão (verify: true) ela cosign-verifica ghcr.io/martinez1991/quorum-sec-scan:full (instalando o cosign se preciso) antes de executar a imagem via docker run. Recomenda-se fixar a imagem por @sha256:... em produção (input image).
Binário (a partir de um release):
cosign verify-blob checksums.txt \
--signature checksums.txt.sig \
--certificate checksums.txt.pem \
--certificate-identity-regexp \
"https://github.com/Martinez1991/quorum-sec-scan/.github/workflows/release.yml@.*" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
sha256sum -c checksums.txt
6. Rollback¶
Como os artefatos são imutáveis e versionados, rollback é uma operação de re-apontamento/pin, não de "desfazer um deploy".
Para o consumidor (recomendado)¶
- Pin por versão exata: trocar
:latest/:fullpor:<versão-anterior>-full(ex.::0.2.2-full) ou, idealmente, pelo digest@sha256:...de uma release boa conhecida. - Action: trocar
@v0por uma tag fixa anterior, ou fixarimage:no digest desejado.
| Cenário | Ação de rollback |
|---|---|
| Imagem nova com regressão | Repin para :<versão-anterior>-full ou digest anterior |
| Binário com bug | Baixar o asset da release anterior (assinada) |
Action @v0 instável |
Pin @vX.Y.Z específico ou pin do image: por digest |
Para o mantenedor¶
- Re-tag da tag móvel: mover
:full/:latest/:v0de volta para o digest da release anterior. O cosign assina o digest, então a assinatura/atestação da release anterior permanece válida ao re-apontar a tag. - Forward-fix preferível: cortar uma nova tag semver (
vX.Y.Z+1) com a correção é o caminho mais limpo, pois cada release é integralmente reconstruída, assinada e atestada — evitando estados ambíguos.
Por design, não se sobrescreve uma tag semver existente com conteúdo diferente. A tag semver é imutável; tags móveis (
full,latest,v0) é que se reposicionam.
Checklist de rollback¶
- [ ] Identificar a última versão boa conhecida (tag semver + digest).
- [ ] Consumidores: repin para versão/digest anterior (ou
@vX.Y.Zdo Action). - [ ] Mantenedor: se necessário, re-apontar tags móveis para o digest anterior.
- [ ] Confirmar
cosign verify+gh attestation verifyno artefato de destino. - [ ] Abrir forward-fix e cortar nova tag semver assim que possível.
7. Feature flags, Blue-Green e Canary¶
N/A para o produto. Estas técnicas pressupõem um serviço de longa duração com tráfego ao vivo que possa ser roteado, alternado ou gradualmente migrado. O Quorum é um processo CLI/Docker efêmero: executa, produz um relatório (SARIF/JSON/XML) e termina. Não há tráfego, não há réplicas em paralelo, não há estado de runtime para alternar.
| Técnica | Status | Justificativa |
|---|---|---|
| Feature flags | N/A | Comportamento é controlado por flags da CLI em tempo de invocação (--scanners, --format, --fail-on, --offline, ...); não há toggles dinâmicos de runtime |
| Blue-Green | N/A | Não há ambiente "live" com pool de tráfego para alternar |
| Canary | N/A (no produto) | Não há frota servindo requisições para liberar gradualmente |
Como o CONSUMIDOR pode fazer canary de versões¶
Embora o produto não suporte canary internamente, o pipeline consumidor pode fazer canary de versões do Quorum com estratégias padrão de CI:
- Pin escalonado: a maioria dos repositórios fixa uma versão estável (digest/
@vX.Y.Z); um repositório piloto adota a nova versão antes da adoção ampla. - Execução paralela não bloqueante: rodar a versão nova em um job com
continue-on-error: truelado a lado com a versão estável (gate), comparando osummarye oexit-codeantes de promover. workflow_dispatch/ matriz de versões: comparar relatórios de duas tags (estável vs. candidata) sobre o mesmo alvo e só então atualizar o pin.- Comparação de relatórios determinística: usar
--offlinepara reduzir variância de rede ao compararmultiDetected/detectionCountentre versões.
flowchart LR
repoA[Repos em producao<br/>pin estavel @vX.Y.Z] --> use[Quorum estavel]
repoP[Repo piloto / job canary] --> cand[Quorum candidata @vX.Y.Z+1]
cand -->|compara summary/exit-code| dec{ok?}
dec -->|sim| promote[promover pin para nova versao]
dec -->|nao| keep[manter pin anterior]
Feature flag de comportamento de scan = simplesmente escolher flags da CLI por invocação. Não há, nem é desejável, um sistema de flags dinâmico para um binário de uma execução só.
8. Resumo de gatilhos e responsabilidades¶
| Workflow | pull_request |
push: main |
tag vX.Y.Z |
workflow_dispatch |
|---|---|---|---|---|
ci.yml (build-test) |
sim | sim | — | — |
e2e.yml (consensus) |
sim | sim | — | sim |
release.yml (images+binaries) |
— | — | sim | sim |
| Artefato | Onde | Assinatura | Proveniência | Multi-arch |
|---|---|---|---|---|
Imagem :full |
GHCR | cosign keyless | SLSA (verificada) | linux/amd64 |
Imagem :slim |
GHCR | cosign keyless | SLSA (verificada) | linux/amd64, linux/arm64 |
| Binários | GitHub Release | cosign sign-blob (sobre checksums) | SLSA (verificada) | linux/darwin/windows x amd64/arm64 |
Premissas¶
- Conteúdo dos Dockerfiles não foi detalhado linha a linha. As variantes
:full/:slime o conteúdo (scanners empacotados, grype DB pré-cacheado) baseiam-se nos comentários derelease.yml, no.goreleaser.yamle na descrição do produto; o passo de build em si (docker/build-push-action@v6) foi documentado a partir do workflow, não da análise interna deDockerfile/Dockerfile.full. secrets.GITHUB_TOKENé o token padrão fornecido pelo GitHub Actions; assume-se que os escopospackages: write,id-token: writeeattestations: writeestejam disponíveis no repositório (são declarados no workflow).- Branch protection / required checks: o documento descreve o fluxo via PR para
maincomo prática; não há arquivo de configuração de proteção de branch no repositório verificável aqui, então as regras de "checks obrigatórios" são recomendações alinhadas ao comportamento dos workflows. - Comandos de verificação para o consumidor (cosign/
gh attestation verify) foram extrapolados dos comentários e dos passos de verificação presentes emrelease.ymleaction.yml; o regexp de identidade usa o ownerMartinez1991, conformeaction.yml. v0como tag móvel do Action: confirmado pelo comentário emaction.yml(uses: Martinez1991/quorum-sec-scan@v0) e pelo gatilho restrito a semver emrelease.yml. O reposicionamento dev0/v0.2é automatizado pelo workflow.github/workflows/tag-major.yml, que roda a cada release semver e avança as tags móveis para o mesmo commit (essas tags não re-disparamrelease.yml/tag-major.yml).
Ver também: README.md · README.pt-BR.md · DESIGN.md (supply chain §12, status de scanner §14).