Testes¶
Esta seção descreve a estratégia de testes do Quorum (quorum-sec-scan, v0.2.3): o que já existe no código hoje (as-is), como executar, e o que está proposto como evolução. O Quorum é uma ferramenta CLI/Docker de consensus security scanning escrita em Go 1.26; portanto, partes clássicas de um plano de testes corporativo que pressupõem frontend web, banco de dados relacional ou API REST são declaradas N/A com justificativa técnica. O princípio de produto "false split > false merge" e o axioma operacional "0 findings is not proof of safety" guiam também as escolhas de teste: preferimos quebrar cedo (contract tests, -race, gates de consenso no E2E) a passar silenciosamente.
Documentos relacionados: Arquitetura · CI/CD · Supply chain · DESIGN.md (§5 contract tests, §6 matriz de correlação, §14 status de scanner).
1. Visão geral da pirâmide de testes¶
O Quorum concentra peso na base (unitários + contract tests determinísticos sobre fixtures) e mantém um topo enxuto, porém real (E2E com scanners de verdade). Não há camada de testes de UI porque não há UI.
flowchart TB
E2E["E2E real (e2e.yml)<br/>scanners reais → consenso cross-engine<br/>alpine:3.10 + examples/terraform"]
INT["Integração / pipeline<br/>parse real → correlate → crosswalk → consensus<br/>(realdata_test.go, pipeline_test.go)"]
UNIT["Unitários + Contract tests<br/>adapters, orchestrator, alias, cache,<br/>filter, severity, report, cmd"]
UNIT --> INT --> E2E
style UNIT fill:#dff0d8,stroke:#3c763d
style INT fill:#fcf8e3,stroke:#8a6d3b
style E2E fill:#d9edf7,stroke:#31708f
| Camada | Onde vive | Determinístico? | Roda em | Gate de PR |
|---|---|---|---|---|
| Unitário | internal/**/<pkg>_test.go, cmd/quorum/scan_test.go |
Sim | go test -race ./... (ci.yml) |
Sim |
| Contract (parsers) | internal/adapter/adapter_test.go + realdata_test.go vs internal/adapter/testdata/*.json |
Sim (fixtures versionadas) | go test ./... |
Sim |
| Integração (pipeline) | internal/adapter/realdata_test.go, internal/correlate/pipeline_test.go |
Sim (fixtures + crosswalk real do repo) | go test ./... |
Sim |
| E2E (consenso real) | .github/workflows/e2e.yml |
Não (rede/DBs externos) | GitHub Actions | Sim (push/PR/manual) |
2. Como executar localmente¶
Targets relevantes do Makefile:
make test # go test ./... → unitários + contract + integração
make vet # go vet ./...
make all # vet + test + build
make build # binário em ./dist/quorum
make run ARGS="scan <target> --type image --scanners trivy,grype"
Variações úteis (não embrulhadas em target, mas suportadas):
go test -race ./... # detector de corrida (igual ao CI)
go test ./internal/adapter/... # só os contract tests dos adapters
go test -run TestMVPConsensus ./internal/correlate
go test -cover ./... # cobertura por pacote (resumo)
go test -coverprofile=cover.out ./... && go tool cover -func=cover.out
O CI usa exatamente
go test -race ./...(ver CI/CD), então rodar com-racelocalmente reproduz o gate.
3. Testes unitários (atual)¶
Cobrem a lógica pura e as regras de negócio, sem dependências externas (sem rede, sem binários de scanner — exceto os contract tests, que leem fixtures de disco).
| Pacote | Arquivo | O que verifica |
|---|---|---|
internal/severity |
severity_test.go |
FromCVSS (faixas → severidade), FromDockle (FATAL/WARN/INFO), Max, AtLeast, Parse. |
internal/filter |
filter_test.go |
--min-severity (Apply), baseline .quorumignore (match por fingerprint case-insensitive e por correlationKey), contadores SuppressedSeverity/SuppressedBaseline, baseline ausente e Has em baseline vazio. |
internal/cache |
store_test.go |
Put/Get/persistência em disco (cria diretório-pai), reabertura, segurança em *Store nil e modo in-memory (path vazio). |
internal/alias |
alias_test.go |
Cliente OSV via httptest (aliases, erro 500, retry em 503, sem retry em 404), preferência por aliases locais (zero chamadas de rede), cache após primeira resolução, degradação graciosa em falha de rede (id inalterado). |
internal/orchestrator |
orchestrator_test.go |
Fan-out e merge cross-scanner (fakeAdapter), status ran/unavailable/error, scanner desconhecido descartado com warning, probe-timeout (ProbeTime) marcando unavailable com erro "version probe exceeded" e log "timed out". |
internal/report |
report_test.go |
SARIF (schema 2.1.0, partialFingerprints["quorum/v1"], fingerprint, level: error, detectionCount), JSON (summary.totalFindings/multiDetected), XML (<?xml, quorumReport, detectionCount), ParseFormat (aceita sarif/json/xml case-insensitive, rejeita pdf). |
cmd/quorum |
scan_test.go |
resolveCrosswalkDir (flag explícita verbatim, default existente, fallback para /opt/quorum/crosswalk quando default ausente, retorno original quando nenhum existe) e isDir. |
Pontos de atenção (boas práticas já adotadas no código):
- Tabelas de casos (
TestFromCVSS,TestRealParse_Counts). t.TempDir()para isolamento de FS (cache, baseline, crosswalk).httptest.NewServerpara isolar a dependência de rede do OSV.dev — nenhum teste unitário fala com a internet real.- Subtests
t.Runcomt.Skipf/t.Skipquando o ambiente não satisfaz a pré-condição (ex.:bundledCrosswalkDirsó existe na imagem Docker).
4. Contract tests dos adapters (atual)¶
São o guarda-formato: parseiam saídas reais e versionadas de cada scanner e quebram antes da produção quando uma ferramenta muda seu JSON (DESIGN §5). Vivem em internal/adapter/ e leem fixtures de internal/adapter/testdata/.
Fixtures presentes:
internal/adapter/testdata/
├── trivy_image.json grype_image.json
├── sca_trivy_alpine.json sca_grype_alpine.json
├── iac_trivy.json iac_trivy_v071.json
├── iac_checkov.json iac_kics.json
└── img_dockle_alpine.json
Garantias travadas pelos contract tests:
flowchart LR
F["fixture JSON<br/>(saída real do scanner)"] --> P["adapter.parse()"]
P --> C["model.Finding canônico"]
C --> A{"asserções"}
A --> A1["contagem de findings"]
A --> A2["VulnID / PURL / Severity / CVSS"]
A --> A3["Confirmed (authoritative)"]
A --> A4["CanonicalControl (AVD/CIS)"]
A --> A5["Aliases (CVE/GHSA)"]
Casos notáveis (e por que existem):
TestTrivyParse/TestGrypeParse: mapeamento de campos canônicos (PURL, severidade, CVSS,Confirmed), e fallback de versão do Grype para odescriptor.TestRealParse_Counts: contagens exatas por fixture (trivy-iac=12, checkov-iac=17, kics-iac=9, trivy-sca=1, grype-sca=1) e invariantes (Scanner/Titlenão vazios).TestTrivyV071AVDPrefix: trava a correção de drift — Trivy ≥ ~0.60 passou a emitir ids "AWS-0086" sem prefixo; o adapter restauraAVD-AWS-0086para casar com o crosswalk. (Mesma família do fix do commit recente de "relax version probe / unknown scanners".)TestDockleParse: linhas PASS/SKIP/IGNORE são descartadas; FATAL/WARN/INFO viram findingsImgHardening; códigosCIS-DI-*já canônicos; mapeamento de severidade WARN→MEDIUM, INFO→LOW.
Manutenção: ao subir a versão suportada de um scanner, capture uma nova fixture (rodando o scanner via imagem oficial contra
examples/terraformoualpine:3.10), adicione-a atestdata/e fixe a contagem esperada — exatamente o padrão deiac_trivy_v071.json.
5. Testes de integração / pipeline (atual)¶
Diferem dos unitários por exercitar o caminho real end-to-end de dados (sem rede): parse → correlate.Enrich → crosswalk → consensus.Merge, usando o crosswalk real do repositório (../../crosswalk).
| Teste | Arquivo | Invariante de produto |
|---|---|---|
TestMVPConsensus |
correlate/pipeline_test.go |
Trivy(CVE) + Grype(GHSA) → após resolução de alias, 1 finding, detectionCount=2, confidence∈(0,1]. Critério "done" do MVP. |
TestUnmappedNeverMerges |
correlate/pipeline_test.go |
Misconfigs sem controle canônico resolvido não se fundem (false split > false merge). |
TestCrosswalkMerges |
correlate/pipeline_test.go |
Com regra de crosswalk mapeando Checkov + Trivy ao mesmo AVD-AWS-0091, eles se fundem (detectionCount=2). Exercita o loader YAML real. |
TestRealIaCConsensus |
adapter/realdata_test.go |
Trivy+Checkov+KICS sobre o mesmo Terraform: consenso 3-way em AVD-AWS-0090/0089/0092/0057; 2-way em AVD-AWS-0132. |
TestRealSCAConsensus |
adapter/realdata_test.go |
Trivy+Grype concordam em CVE-2021-36159 (apk-tools) → 1 finding, detectionCount=2, severidade CRITICAL. |
Esses testes são a prova determinística de que o consenso acontece sobre dados reais — o E2E (§6) é a prova não determinística equivalente com binários ao vivo.
6. Testes E2E (atual)¶
.github/workflows/e2e.yml — "End-to-end proof that consensus actually happens with REAL scanners (not fixtures)". Dispara em push para main, em pull_request e em workflow_dispatch.
Fluxo:
flowchart TB
B["Build quorum (go build)"] --> I["Instala scanners reais<br/>trivy 0.71.2, grype 0.114.0, checkov (pipx)"]
I --> V["Verifica versões + grype db update + docker pull alpine:3.10 + quorum list-scanners"]
V --> IAC["IaC: quorum scan examples/terraform<br/>--scanners trivy,checkov --offline"]
V --> SCA["SCA: quorum scan alpine:3.10<br/>--scanners trivy,grype --offline"]
IAC --> GI{"summary.multiDetected >= 1 ?"}
SCA --> GS{"summary.multiDetected >= 1 ?"}
GI -- não --> FAIL["::error:: sem consenso IaC → falha"]
GS -- não --> FAIL2["::error:: sem consenso SCA → falha"]
GI -- sim --> OK
GS -- sim --> OK["upload iac.json + sca.json"]
Características-chave:
- Gate de consenso explícito: o job falha se
jq '.summary.multiDetected'for< 1em qualquer cenário — ou seja, falha se nenhum finding for corroborado por dois engines. Isso protege contra regressões silenciosas na correlação. - Mesma imagem local nos dois scanners (
docker pull alpine:3.10) para garantir que Trivy e Grype resolvam o mesmo artefato. grype db updateroda cedo para falhar caso o DB de vulnerabilidades não possa ser obtido.--offlinedesliga o OSV.dev nos cenários (a resolução depende de aliases locais), reduzindo flakiness de rede.- Relatórios
iac.json/sca.jsonsão publicados como artefatos (if: always()).
N/A — DAST de aplicação: não há um servidor/endpoint para atacar; o "E2E" aqui é de pipeline de scanning, não de aplicação web.
7. Testes de carga, stress e chaos (propostos)¶
Não existem hoje. Por ser CLI batch (sem servidor de longa duração), as métricas relevantes são throughput de scan e degradação sob recurso escasso, não RPS.
7.1 Carga (proposta)¶
- Objetivo: medir tempo total e uso de memória ao escanear targets grandes (monorepos, imagens com muitos pacotes) com o pool completo de 6 scanners em fan-out.
- Como: matriz de targets sintéticos crescentes + medição de
--timeout, picos de RSS e tempo de parede; assertar regressão contra baseline. - Ferramenta sugerida:
hyperfinepara wall-clock,/usr/bin/time -vpara RSS, mais um harness Go opcional (go test -bench) para o pipelinecorrelate/consensus.
7.2 Stress (proposta)¶
- Objetivo: comportamento quando um scanner é morto por OOM ou estoura
--timeout/ProbeTime. O orchestrator já distinguetimeout/killed(OOM)/unavailable; um teste de stress confirmaria isso ao vivo. - Como: rodar em container com
--memorybaixo e targets pesados; asserir que o status por scanner é reportado corretamente e que o relatório nunca afirma "0 findings = safe".
7.3 Chaos (proposta)¶
- Objetivo: robustez a dependências instáveis: OSV.dev intermitente/lento, registry inacessível, DB do Grype ausente, fixture/crosswalk corrompido.
- Como: injeção de falha (proxy que devolve 5xx/latência para OSV, bloqueio de rede, crosswalk inválido) e verificação da degradação graciosa (já coberta unitariamente em
alias_test.go, faltando o equivalente E2E). - Ferramenta sugerida:
toxiproxypara a rede; jobs dedicados de "chaos" no Actions.
8. Testes de segurança (SAST/DAST/IAST/SCA/Container/IaC)¶
O Quorum faz dogfooding: ele é uma ferramenta de segurança, então usa a si mesmo (e seus próprios scanners OSS) para cobrir partes desta matriz. Os exemplos de examples/terraform, examples/k8s e examples/ci servem tanto de fixture quanto de alvo de dogfooding.
| Disciplina | Status | Como hoje / proposta | Ferramenta |
|---|---|---|---|
| SCA (dependências) | Parcial via dogfooding | E2E escaneia alpine:3.10 com trivy+grype; propor quorum scan . sobre o próprio repo Go + govulncheck no CI |
Trivy, Grype, (propor) govulncheck |
| IaC scan | Atual via dogfooding | E2E escaneia examples/terraform com trivy+checkov (consenso); cobre KICS nos testes de integração |
Trivy, Checkov, KICS |
| Container scan | Atual via dogfooding | E2E sobre alpine:3.10; propor escanear as próprias imagens :full/:slim no release.yml |
Trivy, Grype, Dockle |
| SAST | Proposto | go vet já roda no CI; propor golangci-lint + gosec + CodeQL (Go) |
golangci-lint, gosec, CodeQL |
| DAST | N/A | Não há superfície de runtime (sem API/servidor/web) para atacar dinamicamente | — |
| IAST | N/A | IAST requer instrumentação de app em execução servindo requisições; modelo CLI batch não se aplica | — |
| Supply chain / provenance | Atual | Imagens e binários assinados keyless (cosign OIDC) + atestação SLSA build-provenance, re-verificada no release.yml (gh attestation verify); GitHub Action composite cosign-verifica a :full antes de rodar |
cosign, actions/attest-build-provenance, gh attestation |
| Secret scanning | Proposto | Não há varredura de segredos própria; propor gitleaks no CI |
gitleaks |
Dogfooding como teste de segurança: rodar
quorum scancontra os próprios artefatos do projeto (repo, Terraform de exemplo, imagens publicadas) é simultaneamente um teste funcional e a varredura de segurança IaC/Container/SCA do próprio Quorum.
Checklist de hardening do CI de segurança (proposta):
- [ ] Adicionar
golangci-lint+gosecaoci.yml. - [ ] Habilitar CodeQL (Go) em workflow dedicado.
- [ ] Job que roda
quorum scan .(dogfooding SCA/IaC) com--fail-on high. - [ ] Escanear as imagens
:full/:slimrecém-construídas norelease.ymlantes do push. - [ ]
govulncheck ./...como gate. - [ ]
gitleakspara segredos.
9. Testes de API / CLI¶
N/A — API REST/HTTP: o Quorum não expõe API HTTP, então não há testes de contrato de API, OpenAPI ou fuzzing de endpoint.
CLI (a "API" real do produto): parcialmente coberto e com lacunas claras.
| Aspecto da CLI | Status | Onde |
|---|---|---|
Resolução de --crosswalk (flag/default/fallback) |
Atual | cmd/quorum/scan_test.go (resolveCrosswalkDir) |
list-scanners (smoke) |
Atual | ci.yml (Smoke) e e2e.yml (Verify) |
scan real ponta-a-ponta com flags --scanners/--format/--output/--offline |
Atual (E2E) | e2e.yml |
Parsing/validação de flags (--type, --format, --fail-on, --min-severity, --timeout) |
Lacuna | propor testes de tabela em cmd/quorum |
Exit codes (0 ok / 1 gate --fail-on / 2 erro de uso/runtime) |
Lacuna | propor testes E2E asserindo $? por cenário |
--quiet, --baseline, --cache |
Lacuna | propor cobertura dedicada |
Proposta de teste de exit codes (acionável no E2E):
# 0 = nenhum finding atingiu --fail-on
quorum scan examples/terraform --type repo --min-severity critical --fail-on critical; test $? -eq 0
# 1 = gate disparou
quorum scan alpine:3.10 --type image --fail-on low; test $? -eq 1
# 2 = uso inválido
quorum scan; test $? -eq 2
10. Performance¶
Não há benchmarks no código hoje. Pontos quentes candidatos a testing.B:
consensus.Mergeecorrelate.Enrich(agrupamento porcorrelationKey, hashing de fingerprint) sobre conjuntos grandes de findings.- Geração de relatório SARIF/JSON/XML em volumes altos.
- Fan-out do orchestrator (overhead de goroutines/timeouts).
Proposta:
Métricas-alvo a estabelecer como baseline: tempo do pipeline de merge para N findings (ex.: 1k, 10k), alocações/op, e wall-clock de um scan completo :full contra um target de referência.
11. Cobertura¶
11.1 Como medir (atual)¶
A cobertura não é coletada no CI hoje, mas a toolchain Go a fornece nativamente:
go test -coverprofile=cover.out ./...
go tool cover -func=cover.out # resumo por função + total
go tool cover -html=cover.out -o cover.html
go test -covermode=atomic -coverpkg=./... ./... # cobertura cruzada entre pacotes
11.2 Estado de fato por pacote¶
| Pacote | Tem _test.go? |
|---|---|
internal/adapter |
Sim (contract + realdata) |
internal/orchestrator |
Sim |
internal/correlate (+ consensus, alias, crosswalk via pipeline) |
Sim |
internal/alias |
Sim |
internal/cache |
Sim |
internal/filter |
Sim |
internal/severity |
Sim |
internal/report |
Sim |
cmd/quorum |
Sim (parcial: crosswalk/isDir) |
internal/model, internal/purl, internal/consensus, internal/crosswalk |
Sem _test.go dedicado (exercitados indiretamente pelo pipeline) |
11.3 Metas (propostas)¶
- Meta global: ≥ 80% de cobertura de linhas (
-coverpkg=./...). - Componentes críticos (
correlate,consensus,crosswalk,severity,report): ≥ 90% — são o núcleo do produto. - Adicionar coleta no
ci.yml(upload docover.outcomo artefato e, opcionalmente, gate de "não regredir"). - Fechar lacunas: testes diretos para
internal/purleinternal/consensus; ampliarcmd/quorumpara flags e exit codes.
Checklist de cobertura:
- [ ] Coletar
-coverprofilenoci.ymle publicar artefato. - [ ] Definir threshold mínimo global e por pacote crítico.
- [ ] Adicionar testes unitários diretos para
internal/purleinternal/consensus. - [ ] Cobrir parsing de flags e exit codes em
cmd/quorum.
12. Matriz mestre: tipo → status → ferramenta¶
| Tipo de teste | Status | Onde / como | Ferramenta |
|---|---|---|---|
| Unitário | Atual | internal/**/*_test.go, cmd/quorum/scan_test.go |
go test -race |
| Contract (formato de scanner) | Atual | internal/adapter/*_test.go + testdata/*.json |
go test, fixtures versionadas |
| Integração (pipeline) | Atual | realdata_test.go, pipeline_test.go (crosswalk real) |
go test |
| E2E (consenso real) | Atual | .github/workflows/e2e.yml |
trivy, grype, checkov, jq, Actions |
| Smoke (CLI) | Atual | ci.yml / e2e.yml (list-scanners) |
binário compilado |
| Race detection | Atual | ci.yml (go test -race) |
Go race detector |
| Estático (vet) | Atual | ci.yml (go vet) |
go vet |
| Cobertura | Proposto | coletar no CI; metas §11 | go tool cover |
| Performance / Benchmark | Proposto | consensus/correlate/report |
go test -bench, hyperfine |
| Carga | Proposto | targets grandes, fan-out 6 scanners | hyperfine, time -v |
| Stress (OOM/timeout) | Proposto | container com --memory baixo |
Docker limits |
| Chaos (deps instáveis) | Proposto | OSV 5xx/latência, registry off | toxiproxy |
| SAST | Proposto | lint/sec estático do código Go | golangci-lint, gosec, CodeQL |
| SCA | Parcial / dogfooding | E2E alpine:3.10; propor self-scan + govulncheck |
Trivy, Grype, govulncheck |
| IaC scan | Atual / dogfooding | E2E examples/terraform |
Trivy, Checkov, KICS |
| Container scan | Atual / dogfooding | E2E alpine:3.10; propor self-scan das imagens |
Trivy, Grype, Dockle |
| DAST | N/A | sem superfície de runtime | — |
| IAST | N/A | sem app instrumentável em execução | — |
| API REST testing | N/A | sem API HTTP | — |
| Supply chain / provenance | Atual | re-verificação de atestação no release.yml; Action cosign-verifica :full |
cosign, SLSA attest, gh attestation |
| Secret scanning | Proposto | varredura de segredos no repo | gitleaks |
| Exit codes (CLI) | Proposto | assertar $? por cenário no E2E |
shell + Actions |
13. Backlog priorizado de testes (acionável)¶
- [ ] P0 — Coletar cobertura no
ci.ymle estabelecer baseline (§11). - [ ] P0 — Testes de exit codes (0/1/2) no E2E, cobrindo
--fail-on(§9). - [ ] P1 — SAST no CI:
golangci-lint+gosec+ CodeQL (§8). - [ ] P1 — Dogfooding no CI:
quorum scan .e self-scan das imagens publicadas (§8). - [ ] P1 —
govulncheck ./...como gate (§8). - [ ] P2 — Testes diretos para
internal/purleinternal/consensus(§11). - [ ] P2 — Benchmarks de
consensus/correlate/report(§10). - [ ] P3 — Chaos de rede para OSV.dev via
toxiproxy(§7.3). - [ ] P3 — Stress de OOM/timeout em container com memória limitada (§7.2).
Premissas¶
- As-is verificado em código: as afirmações de "Atual" foram conferidas lendo
Makefile,.github/workflows/ci.yml,.github/workflows/e2e.yml,.github/workflows/release.ymle todos os*_test.golistados em §3–§5. "Proposto" indica explicitamente o que não existe hoje. - Versão do produto: v0.2.3; Go 1.26 (conforme
go.mode os workflows que fixamgo-version: "1.26"). -raceno CI: oci.ymlusago test -race ./...; oMakefile testusago test ./...(sem-race). Ambos os comandos são apresentados como válidos.- Cobertura não é coletada hoje no CI — todas as metas numéricas de cobertura (80%/90%) são propostas, não estado atual.
- E2E não é determinístico: depende de releases de scanners (trivy 0.71.2, grype 0.114.0, checkov via pipx), do DB do Grype e da imagem
alpine:3.10; mudanças upstream podem afetar contagens. Por isso os gates do E2E checam apenasmultiDetected >= 1, não contagens exatas (essas ficam nos contract tests determinísticos). - DAST/IAST/API REST = N/A porque o Quorum é CLI/Docker batch, sem servidor, API HTTP, frontend, banco relacional, autenticação ou componente de IA/LLM.
- Dogfooding assume que
examples/terraform,examples/k8seexamples/cipermanecem como alvos válidos de scan no repositório.
Lacunas conhecidas (gaps)¶
- Não foi inspecionado o conteúdo de cada fixture JSON individualmente além do que os testes asseguram; contagens citadas vêm das asserções dos testes.
- Não há números reais de cobertura medidos neste documento (a coleta é proposta).
internal/model,internal/purl,internal/consensuseinternal/crosswalknão possuem_test.godedicado próprio — são exercitados indiretamente; a profundidade exata dessa cobertura indireta não foi quantificada.