Frontend / Experiência de Terminal¶
O Quorum não possui frontend gráfico (web, desktop ou mobile). Ele é, por
design, uma ferramenta CLI/Docker orientada a CI/CD: "configure via flags,
gate via exit code. No panel, no daemon" (cmd/quorum/root.go).
A "interface de usuário" do Quorum é, portanto, a experiência de terminal:
logs de progresso em stderr com prefixo [quorum], uma tabela de summary
ao final, exit codes determinísticos para gating, e o artefato SARIF — que,
quando consumido pelo GitHub Code Scanning, vira a superfície visual mais
próxima de uma GUI. Este documento descreve essa UX como ela existe no código
(as-is), trata o que não existe como N/A com justificativa, e separa
claramente eventuais propostas futuras.
1. Veredito: GUI / Web — N/A¶
| Item de template típico | Status | Justificativa técnica (as-is) |
|---|---|---|
| SPA / aplicação web (React, Vue, etc.) | N/A | Não há código de UI no repositório. O binário é um CLI Cobra (cmd/quorum). main() apenas executa o comando raiz e sai. |
| Servidor HTTP / API REST que sirva uma UI | N/A | Não há servidor; não há net/http handlers de aplicação. O Quorum é stateless e one-shot: roda, emite relatório, encerra. |
| Painel/dashboard, daemon, long-running | N/A | Princípio explícito: "No panel, no daemon" (root.go). Cada invocação é um processo efêmero. |
| Autenticação / contas / sessões de usuário | N/A | Não há multiusuário nem persistência de sessão. A única persistência é o cache local de aliases (~/.cache/quorum/aliases.json). |
| Componentes visuais, CSS, design system web | N/A | A renderização é texto puro em stderr/stdout. Sem ANSI/cores no código Go. |
| Responsividade / breakpoints / mobile | N/A | Conceito de viewport não se aplica a um TUI não-interativo. Ver §8 Responsividade. |
Por que isso é uma decisão, não uma lacuna. O alvo do Quorum é o runner de CI/CD e o terminal do desenvolvedor. Um painel web exigiria servidor, estado, autenticação e superfície de ataque — tudo contrário ao modelo de ameaça de uma ferramenta de segurança que roda dentro do pipeline. A "visualização" rica é delegada a sistemas já existentes (GitHub Code Scanning, qualquer viewer SARIF), via o artefato SARIF padronizado.
A superfície visual mais próxima de uma GUI é tratada em §11 GitHub Code Scanning.
2. Anatomia da experiência de terminal¶
A UX de terminal do Quorum tem três canais bem separados, o que é deliberado para permitir piping do relatório sem poluí-lo com logs:
flowchart LR
subgraph CLI["quorum scan <target>"]
A["Progresso / diagnóstico<br/>prefixo [quorum]"] --> E2["stderr"]
B["Tabela de summary<br/>── quorum summary ──"] --> E2
C["Relatório SARIF/JSON/XML"] --> O1["stdout (ou --output arquivo)"]
D["Exit code 0 / 1 / 2"] --> X["$?"]
end
E2 --> H["Humano / logs do runner"]
O1 --> P["Pipe / upload (ex: SARIF → Code Scanning)"]
X --> G["Gate do pipeline"]
| Canal | Conteúdo | Quando aparece | Controlado por |
|---|---|---|---|
stdout |
Relatório (SARIF/JSON/XML) | Sempre que --output/-o não é dado |
--format, --output |
stderr |
Logs de progresso [quorum] … + tabela de summary |
Durante o scan e ao final | --quiet/-q |
| exit code | 0 ok, 1 gate disparou, 2 erro de uso/runtime |
Ao encerrar | --fail-on |
Garantia de separação. Como o relatório vai para
stdoute todo o ruído vai parastderr,quorum scan img -f json > report.jsonproduz um JSON limpo mesmo sem--quiet. A tabela de summary nunca contaminastdout.
3. Logs de progresso ([quorum] …)¶
Os logs de progresso são emitidos pela closure logf em
runScan, que prefixa toda linha com [quorum] e
escreve em stderr — a menos que --quiet esteja ativo:
logf := func(format string, args ...any) {
if !f.quiet {
fmt.Fprintf(os.Stderr, "[quorum] "+format+"\n", args...)
}
}
Essa mesma logf é injetada no orquestrador via orchestrator.Options.Logf,
de modo que o pipeline inteiro fala pelo mesmo canal e com o mesmo prefixo.
3.1 Catálogo de mensagens de progresso¶
| Origem | Mensagem (formato) | Significado |
|---|---|---|
runScan (preâmbulo) |
target=… type=… crosswalk=N rules (dir) offline=bool |
Resumo da configuração resolvida antes do fan-out |
orchestrator.Run |
warning: unknown scanner "x" ignored (known: …) |
Nome passado em --scanners não corresponde a adapter registrado |
runOne (skip) |
skip <name>: does not support target <type> |
Adapter não suporta o tipo de alvo → status skipped |
runOne (probe lento) |
skip <name>: version probe timed out after 60s (slow start / low memory?) |
Probe de versão excedeu ProbeTime → status unavailable |
runOne (OOM) |
skip <name>: version probe killed (likely OOM — increase container memory) |
signal: killed no probe → provável OOM → status unavailable |
runOne (ausente) |
skip <name>: not installed/available |
Binário não instalado/encontrado → status unavailable |
runOne (execução) |
run <name> (<ver>) ... |
Scanner iniciou |
runOne (sucesso) |
done <name>: N findings in <dur> |
Scanner terminou com N achados brutos → status ran |
runOne (falha) |
fail <name>: <err> |
Scanner falhou (status error) ou estourou timeout (timeout) |
runScan (filtro) |
filtered: N suppressed by baseline (M entries), K below min-severity <sev> |
Pós-processamento por .quorumignore / --min-severity |
runScan (gate) |
gate: found <sev> finding >= --fail-on <thr> → exit 1 |
Gating disparou; processo sairá com código 1 |
Supressões sempre logam. Quando o baseline ou o
--min-severitydescartam achados, o número é registrado emstderr. Isso atende ao princípio de transparência: nada é silenciosamente removido do relatório.
3.2 Sequência típica (sucesso, sem --quiet)¶
sequenceDiagram
participant U as Usuário/CI
participant Q as quorum scan
participant O as orchestrator
participant S as scanners (paralelos)
U->>Q: quorum scan img:tag --fail-on high
Q-->>U: [quorum] target=… type=image crosswalk=… offline=false
Q->>O: Run(target, Options{Logf})
O-->>U: [quorum] run trivy (0.5x) ...
O->>S: fan-out (goroutines)
S-->>O: findings
O-->>U: [quorum] done trivy: 42 findings in 3.1s
O-->>Q: Result{Runs, Merged}
Q-->>U: [quorum] filtered: 3 suppressed by baseline …
Q-->>U: (stdout) <SARIF>
Q-->>U: ── quorum summary ── (stderr)
Q-->>U: exit 0 | 1 | 2
4. Tabela de summary¶
Ao final de cada scan, printSummary escreve em
stderr um bloco legível para humanos. Ele é suprimido por --quiet
(retorna imediatamente quando quiet == true). Layout real produzido pelo
código:
── quorum summary ───────────────────────────
trivy ran 42 findings
grype ran 38 findings
checkov unavailable 0 findings (version probe killed — likely out…)
kics skipped 0 findings
dockle ran 5 findings
kubescape timeout 0 findings (context deadline exceeded)
----------------------------------------
57 findings after consensus (31 multi-detected)
CRIT 4 HIGH 12 MED 28 LOW 11 INFO 2
elapsed 7.412s
note: 0 findings is not proof of safety — see scanner statuses above.
4.1 Estrutura¶
| Bloco | Conteúdo | Fonte de dados |
|---|---|---|
| Linha por scanner | name, status, contagem de findings brutos, e (se houver) erro truncado em 60 chars |
res.Runs ([]ScannerRun) |
| Erro inline | (…) com truncate(err, 60) quando ScannerRun.Error != "" |
ScannerRun.Error |
| Total pós-consenso | N findings after consensus (M multi-detected) |
len(res.Merged) + DetectionCount>1 |
| Distribuição | CRIT … HIGH … MED … LOW … INFO … |
contagem por m.Severity |
| Tempo | elapsed <dur> arredondado a milissegundos |
res.Duration |
| Nota de cautela | note: 0 findings is not proof of safety … |
literal (DESIGN §14) |
findings brutosvsapós consenso. A coluna por scanner mostra o número de achados que aquele scanner emitiu antes de correlação. A linha de total mostra os achados após o merge de consenso. Por isso a soma das colunas é normalmente maior que o total — é esperado e desejável (vários scanners detectando o mesmo CVE colapsam em um achado comDetectionCount > 1).
5. Estados de scanner (status)¶
Cada scanner termina em exatamente um de cinco estados, definidos em
orchestrator.ScannerRun.Status.
Eles são a peça central da transparência da UX — "0 vulns" nunca pode parecer
"o scan não rodou" (DESIGN §14).
stateDiagram-v2
[*] --> Supports?
Supports? --> skipped: não suporta o alvo
Supports? --> Probe
Probe --> unavailable: probe falhou\n(timeout 60s / OOM / não instalado)
Probe --> Run
Run --> ran: terminou OK
Run --> timeout: estourou --timeout
Run --> error: falha de execução
ran --> [*]
skipped --> [*]
unavailable --> [*]
timeout --> [*]
error --> [*]
| Status | Quando ocorre | Diagnóstico ao usuário |
|---|---|---|
ran |
Supports==true, probe OK, Run retornou sem erro |
Contagem de findings na tabela |
skipped |
Supports(target)==false |
"does not support target …" — não é falha, só não se aplica |
unavailable |
Version() (probe) falhou: timeout (60s), signal: killed (OOM), ausente |
Mensagem distingue slow start, OOM e binário ausente, com sugestão de correção |
timeout |
Run excedeu --timeout (default 5m) → context.DeadlineExceeded |
status timeout + erro inline |
error |
Run retornou erro que não é deadline |
status error + erro inline (truncado a 60 chars no summary) |
Probe de versão de 60s. O orquestrador roda um probe de versão antes de cada scanner com
ProbeTime = 60s(generoso de propósito, porque tools Python como ocheckovtêm cold start lento e podem ser SIGKILLed em runners com pouca memória). É esse probe que distingueunavailable/timeout/OOM de uma execução real.
6. Exit codes e gating¶
Os exit codes são a API de máquina da UX — é como o pipeline lê o resultado.
| Código | Significado | Origem no código |
|---|---|---|
0 |
OK — nenhum achado atingiu --fail-on (ou sem --fail-on) |
runScan retorna nil |
1 |
Gate disparou — algum achado ≥ --fail-on |
os.Exit(1) em runScan após severity.AtLeast(worst, thr) |
2 |
Erro de uso ou runtime (flag inválida, alvo ruim, etc.) | main(): fmt.Fprintln(os.Stderr, "quorum:", err); os.Exit(2) |
Fluxo de decisão do gate:
flowchart TD
A[scan concluído] --> B{--fail-on definido?}
B -- não --> Z[exit 0]
B -- sim --> C[worst = maior severidade entre Merged]
C --> D{worst >= threshold?}
D -- não --> Z
D -- sim --> E["[quorum] gate: … → exit 1"]
E --> F[exit 1]
Checklist para usar gating em CI:
- [ ] Defina
--fail-oncom a severidade-limite (critical|high|medium|low). - [ ] Considere
--min-severitypara remover ruído abaixo de um piso (afeta relatório e gating). - [ ] Trate
exit 2como erro de configuração/infra (não como "achados encontrados"). - [ ] Não confunda
exit 0com "seguro" — confira os status de scanner (unavailable/timeoutmascaram cobertura). - [ ] Em runners com pouca RAM, se vir
unavailable (OOM), aumente a memória do container ou restrinja--scanners.
Cuidado com
exit 0. Umexit 0só garante que nenhum achado atingiu o limiar; não garante que todos os scanners rodaram. Um scannerunavailable/timeoutpode ter deixado uma classe inteira de achados de fora. A nota0 findings is not proof of safetyexiste exatamente por isso.
7. Modo silencioso (--quiet / -q)¶
--quiet desliga tudo que vai para stderr:
- Os logs
[quorum] …(a closurelogfvira no-op). - A tabela de summary (
printSummaryretorna no início sequiet).
O que não é afetado por --quiet:
- O relatório em
stdout/--output(continua sendo emitido normalmente). - O exit code (gating continua funcionando).
- A linha de erro fatal em
main()(quorum: <err>comexit 2).
| Cenário | Sem --quiet |
Com --quiet |
|---|---|---|
Progresso [quorum] … |
sim (stderr) |
não |
| Tabela de summary | sim (stderr) |
não |
Relatório (stdout/-o) |
sim | sim |
| Exit code / gate | sim | sim |
| Erro fatal de uso/runtime | sim (exit 2) |
sim (exit 2) |
Recomendação CI. Use
--quiet -o report.sarif --fail-on high. Você ainda recebe o gate e o arquivo SARIF para upload, sem poluir o log do runner.
8. Responsividade — N/A¶
| Conceito | Status | Justificativa |
|---|---|---|
| Breakpoints | N/A | Não há layout web; nada se reflui por largura de viewport. |
| Layout fluido | N/A | A tabela de summary usa colunas de largura fixa (%-10s %-12s %3d), pensadas para ≥ ~50 colunas de terminal. |
| Mobile/tablet | N/A | O alvo é terminal de CI/desenvolvedor; não há cliente móvel. |
A única consideração de "largura" é o summary: erros são truncados a 60
caracteres (truncate(r.Error, 60)) para não estourar o terminal. Não há
detecção de largura de terminal (COLUMNS) no código.
9. Acessibilidade¶
A UX é texto puro, o que já elimina muitas barreiras (compatível com leitores de tela e braille displays via terminal). Estado atual (as-is):
| Aspecto | Estado (as-is) |
|---|---|
| Cor / ANSI | Não há cor. Nenhum código de escape ANSI é emitido pelo Go. Logo, não há dependência de cor para entender o output. |
NO_COLOR |
Não é necessário tratar NO_COLOR: como já não há cor, a saída é estável independentemente da variável. |
| Texto sem cor | Status (ran/unavailable/…) e severidades (CRIT/HIGH/…) são rótulos textuais, nunca apenas cor. |
| Modo silencioso | --quiet oferece uma saída mínima/determinística para quem quer apenas o arquivo + exit code. |
| Unicode | O cabeçalho usa box-drawing (──) e reticências (…). Requer terminal UTF-8; o conteúdo informativo é ASCII. |
| Idioma das mensagens | As mensagens de runtime são em inglês (no código); esta documentação é pt-BR. |
Checklist de acessibilidade (validação contínua):
- [ ] Garantir que nenhuma informação dependa de cor (já satisfeito: sem ANSI).
- [ ] Manter rótulos textuais para severidade e status (não substituir por ícones-only).
- [ ] Manter o conteúdo de severidade/contagens em ASCII para terminais sem UTF-8.
- [ ] Manter
--quietcomo caminho previsível para automação e ferramentas assistivas.
Proposta futura (claramente separada — não existe hoje). Se cores forem adicionadas algum dia, devem (a) respeitar
NO_COLORe--no-color, (b) só ativar quandostderrfor um TTY, e (c) nunca codificar significado apenas por cor. Nada disso está implementado atualmente.
10. Mensagens de erro e validação¶
A validação acontece cedo, em runScan, e erros
retornam pela cadeia RunE → main(), que imprime quorum: <err> em stderr
e sai com exit 2. Como o comando raiz usa SilenceUsage/SilenceErrors,
não há dump de help a cada erro — só a mensagem objetiva.
| Validação / erro | Mensagem | Exit |
|---|---|---|
--type inválido |
invalid --type "x" (want image\|repo\|k8s) |
2 |
--fail-on inválido |
invalid --fail-on "x" (want critical\|high\|medium\|low) |
2 |
--min-severity inválido |
invalid --min-severity "x" (want critical\|high\|medium\|low) |
2 |
--format inválido |
unknown format "x" (want sarif\|json\|xml) |
2 |
--baseline explícito mas inexistente |
baseline file not found: <path> |
2 |
| Falha ao carregar baseline/crosswalk | loading baseline: … / loading crosswalk: … |
2 |
| Nº de argumentos errado | erro do Cobra (cobra.ExactArgs(1)) |
2 |
Scanner desconhecido em --scanners |
warning: unknown scanner "x" ignored (known: …) (warning, não erro) |
— |
Princípio: erros de configuração são fatais e explícitos (exit 2); situações degradadas mas recuperáveis (scanner ausente, OSV offline, scanner desconhecido na lista) são avisos que não interrompem o scan. Ver degradação graciosa em 09-... quando aplicável.
11. GitHub Code Scanning como superfície visual¶
A representação visual mais rica do Quorum não é dele — é a UI de Code
Scanning do GitHub, alimentada pelo SARIF que o Quorum emite como formato
primário (internal/report/sarif.go).
flowchart LR
Q["quorum scan -f sarif -o results.sarif"] --> A["actions/upload-sarif\n(ou GitHub Action composite do Quorum)"]
A --> CS["GitHub Code Scanning"]
CS --> UI["Aba Security → Code scanning alerts\n(visual, dedup por fingerprint, anotações inline em PR)"]
O que o SARIF do Quorum carrega para essa UI:
| Campo SARIF | Conteúdo do Quorum | Efeito na UI do GitHub |
|---|---|---|
level |
error (CRIT/HIGH), warning (MED), note (resto) |
Severidade/ícone do alerta |
ruleId |
VulnID (CVE), CanonicalControl, RuleID ou correlationKey (fallback) |
Agrupamento por regra |
partialFingerprints["quorum/v1"] |
m.Fingerprint = sha256(correlationKey) |
Dedup estável de alertas entre execuções |
locations |
arquivo + região (linhas) por membro com Location.File |
Anotação inline no diff do PR / navegação para o ponto |
properties.detectedBy / detectionCount |
quais e quantos scanners detectaram | Evidência de consenso (visível ao expandir o alerta) |
properties.confidence |
score de consenso (2 casas) | Sinal de confiança |
properties.correlationKey / unmapped |
chave determinística e flag de não-mapeado | Rastreabilidade e triagem |
run.properties.scanners |
nome/status/versão de cada scanner | Transparência de cobertura no artefato |
partialFingerprintsé o que torna a UI utilizável ao longo do tempo: o GitHub usa esse fingerprint para reconhecer "o mesmo alerta" entre commits, evitando que cada execução crie alertas duplicados. Como o fingerprint do Quorum é determinístico (sha256(correlationKey)), o dedup é estável.
Checklist de integração com Code Scanning:
- [ ] Rodar
quorum scan … -f sarif -o results.sarif(SARIF é o default de--format). - [ ] Fazer upload via
github/codeql-action/upload-sarif(ou a Action composite do Quorum, que verifica a imagem com cosign antes). - [ ] Confirmar permissão
security-events: writeno workflow. - [ ] Usar
--quietno passo de scan para manter o log do runner limpo. - [ ] Opcional: combinar com
--fail-onpara bloquear o PR via exit code, independentemente da UI.
JSON e XML existem como formatos alternativos de máquina (ver
json.go e xml.go), úteis para
ferramentas próprias — mas nenhum deles é uma "tela".
12. Como testar / verificar a UX¶
# 1) Summary + progresso completos, relatório SARIF no stdout
quorum scan alpine:3.18 --fail-on high
# 2) Saída limpa para CI: SARIF em arquivo, sem ruído, com gate
quorum scan alpine:3.18 -q -o results.sarif --fail-on critical
echo "exit=$?"
# 3) Confirmar separação de canais: stdout puro, stderr descartado
quorum scan . -f json 2>/dev/null | jq '.summary'
# 4) Forçar erro de validação (exit 2) e ver a mensagem
quorum scan x --fail-on banana # → quorum: invalid --fail-on "banana" (...)
# 5) Inspecionar estados de scanner (restringindo o pool)
quorum scan . --scanners trivy,inexistente # → warning: unknown scanner ...
Checklist de verificação manual:
- [ ]
stdoutcontém apenas o relatório (nenhum[quorum]). - [ ]
stderrcontém o progresso e a tabela de summary quando sem--quiet. - [ ] Com
--quiet,stderrfica vazio (exceto erro fatal). - [ ]
--fail-onproduzexit 1quando há achado no limiar;exit 0caso contrário. - [ ] Flags inválidas produzem
exit 2com mensagemquorum: …. - [ ] Cada scanner aparece no summary com um dos 5 status.
Premissas¶
- Fonte da verdade é o código. Tudo aqui foi verificado em
cmd/quorum/{main.go,root.go,scan.go},internal/orchestrator/orchestrator.go,internal/report/{report,sarif,json}.goeinternal/severity/severity.gona versão atual do repositório (v0.2.3). - Ausência de cor/ANSI foi inferida de uma busca por
color/NO_COLOR/isatty/IsTerminalno código Go, que não retornou implementação de coloração (a única ocorrência decolorestá emaction.yml, que é a cor do badge da Action no Marketplace, não saída de terminal). Se cor for adicionada no futuro, a seção de Acessibilidade precisará ser revista. - O exemplo de tabela de summary na §4 é
ilustrativo (valores fictícios), mas o layout segue exatamente os
FprintfdeprintSummary. - Detalhes de degradação graciosa (OSV offline, cache de alias, crosswalk fallback) são tocados aqui só onde afetam a UX de terminal; o tratamento completo pertence aos documentos de orquestração/alias.
- Mensagens de runtime estão em inglês no código; esta documentação é pt-BR e traduz o significado, não o literal das strings emitidas.
- Cross-links para outros documentos do diretório
docs/(ex.: alias, supply chain) usam o padrãoNN-arquivo.md; alguns alvos podem ainda não existir no momento da escrita deste arquivo.