Observabilidade¶
O Quorum (quorum-sec-scan, v0.8.3) é uma ferramenta CLI/Docker efêmera: cada execução nasce com quorum scan <target>, roda um pool de scanners em paralelo, emite um relatório e morre. Não há daemon, servidor, banco de dados nem painel — o próprio root.go resume a filosofia: "Built for CI/CD: configure via flags, gate via exit code. No panel, no daemon." Por isso, a observabilidade do Quorum não segue o modelo de serviço de longa duração (métricas push, tracing distribuído, health checks, dashboards ao vivo). Em vez disso, ela se apoia em sinais de processo: logs estruturados em stderr (text ou json), status por scanner, exit codes, um arquivo de métricas Prometheus opt-in e o relatório (SARIF/JSON/XML) como artefato auditável.
Este documento descreve com precisão o que existe hoje (as-is), declara explicitamente o que é N/A para um CLI efêmero (com justificativa técnica) e oferece propostas futuras concretas e claramente separadas do estado atual.
Novidade desde a v0.2.3: os dois recursos que antes eram propostas —
--log-format json(logs NDJSON em stderr) e--metrics <arquivo>(Prometheus textfile) — agora são reais e implementados. Eles saíram da §4 (propostas) e passaram a integrar o estado atual (§2.1 e §2.4).Novidade desde a v0.7.4: a camada de aconselhamento opt-in (
--advice) adiciona sua própria telemetria, com escopo bem delimitado — a série de métricasquorum_advice_*(§2.7) — emitida apenas quando--adviceestá ligado. Sem--advice, o arquivo de métricas e todas as demais saídas são byte-idênticos ao que eram antes. A camada de aconselhamento é somente de apresentação e nunca toca emcorrelationKey/fingerprint/confidence/ severidade agregada / o gate--fail-on.
1. Modelo mental: observabilidade de um CLI efêmero vs. serviço¶
A maioria dos pilares clássicos de observabilidade (Logs, Métricas, Traces) pressupõe um processo de longa duração que pode ser raspado (scraped), consultado e correlacionado ao longo do tempo. O Quorum não é esse processo. Ele é um job batch: executa em segundos a minutos, normalmente dentro de um step de CI ou de um docker run, e termina.
flowchart LR
subgraph "Serviço de longa duração (NÃO é o Quorum)"
S[Processo sempre ativo] --> P[/metrics scrape/]
S --> T[traces para coletor]
S --> H[/healthz/]
end
subgraph "Quorum: job efêmero"
I[quorum scan] --> L[logs em stderr text/json]
I --> ST[status por scanner]
I --> EC[exit code]
I --> MF[arquivo .prom via --metrics]
I --> R[relatório SARIF/JSON/XML]
end
L --> CI[CI logs]
ST --> R
EC --> Gate[gate do pipeline]
MF --> TF[textfile collector / Pushgateway]
R --> CS[Code Scanning / artefato]
Consequência prática: a "telemetria" do Quorum é consumida pela plataforma que o invoca (runner de CI, orquestrador de jobs, shell), não por um stack de observabilidade próprio. Os sinais são projetados para serem capturados pelos logs do step, pelo upload de artefatos, pela ingestão de SARIF (ex.: GitHub Code Scanning) e — quando --metrics é usado — por um coletor textfile ou Pushgateway ao fim do job.
2. Estado atual (as-is)¶
2.1 Logs estruturados em stderr (text ou json)¶
Toda a saída de progresso vai para stderr, mantendo o stdout limpo para o relatório (quando --output não é usado, o relatório sai em stdout — separar os streams é o que permite quorum scan img > report.sarif sem poluição).
Desde a v0.7.4 o formato do log é selecionável pela flag --log-format text|json (default text). No formato text cada evento é prefixado com [quorum]; no formato json cada evento vira uma linha JSON (NDJSON) em stderr. O logger é definido em cmd/quorum/scan.go (runScan):
logf := func(format string, args ...any) {
if f.quiet {
return
}
msg := fmt.Sprintf(format, args...)
if f.logFormat == "json" {
b, _ := json.Marshal(struct {
TS string `json:"ts"`
Level string `json:"level"`
Msg string `json:"msg"`
}{time.Now().UTC().Format(time.RFC3339), "info", msg})
fmt.Fprintln(os.Stderr, string(b))
return
}
fmt.Fprintf(os.Stderr, "[quorum] %s\n", msg)
}
Esse logf é injetado no orquestrador via orchestrator.Options.Logf, de modo que CLI e pipeline compartilham o mesmo canal e formato de log. Um valor inválido de --log-format é recusado com erro de uso (invalid --log-format …) antes de qualquer scanner rodar.
Exemplo de saída no formato json:
{"ts":"2026-07-04T10:00:01Z","level":"info","msg":"run trivy (0.52.2) ..."}
{"ts":"2026-07-04T10:00:04Z","level":"info","msg":"done trivy: 12 findings in 3.14s"}
Eventos logados hoje (linhas reais do código; a mesma mensagem é emitida em ambos os formatos, mudando apenas o envelope):
| Origem | Evento | Exemplo de mensagem (campo msg/texto) |
|---|---|---|
scan.go |
contexto inicial | target=… type=… crosswalk=N rules (…) offline=false |
scan.go |
filtragem/supressão | filtered: N suppressed by baseline (M entries), K below min-severity … |
scan.go |
métricas gravadas | metrics written to <path> |
scan.go |
gate disparado | gate: found HIGH finding >= --fail-on high → exit 1 |
scan.go |
aconselhamento (Fase 0) | advice: knowledge=N entries (<dir>) → M findings enriched |
scan.go |
aconselhamento (RAG) | advice(rag): corpus=N chunks (…) → M findings gained OWASP references |
scan.go |
aconselhamento (IA) | advice(ai): provider=local model=… endpoint=… fix=suggest |
scan.go |
aconselhamento (resultado IA) | advice(ai): N findings gained an AI recommendation (fixes: V verified / P proposed) |
orchestrator.go |
scanner desconhecido | warning: unknown scanner "foo" ignored (known: …) |
orchestrator.go |
skip por não-suporte | skip <name>: does not support target <type> |
orchestrator.go |
probe de versão | skip <name>: version probe timed out after 60s … |
orchestrator.go |
probe OOM | skip <name>: version probe killed (likely OOM …) |
orchestrator.go |
indisponível | skip <name>: not installed/available |
orchestrator.go |
início de execução | run <name> (<ver>) ... |
orchestrator.go |
falha de execução | fail <name>: <erro> |
orchestrator.go |
conclusão | done <name>: N findings in <dur> |
Características e limites (honestos):
- No formato
json, o envelope de cada linha é minimalista e estável: apenasts(RFC 3339 UTC),level(hoje sempre"info") emsg(o texto humano). Não há, por evento, campos tipados comoscanner,eventoudurationMs— esses dados vivem nomsglivre e no relatório. Ou seja,--log-format jsontorna o stream facilmente parseável linha a linha, mas ainda não é um esquema de eventos ricos por campo. - No formato
text, as mensagens têm o prefixo estável[quorum]e um formato razoavelmente parseável, porém em texto livre — sem chave=valor garantida nem timestamps por linha. --quiet/-qsilencia todo o progresso e o summary (controlado porf.quiet), ortogonal ao--log-format: silencia ambos os formatos. Não há um modo--verbose/--debugcom mais granularidade.- Não há rotação, sampling ou correlação de log — desnecessário para um processo efêmero; quem persiste é o runner de CI.
2.2 Status por scanner (o sinal central de "rodou de verdade")¶
O princípio de design "0 findings is not proof of safety" exige que o Quorum nunca confunda "zero vulnerabilidades" com "o scanner não rodou". Isso é materializado pelo ScannerRun.Status (em internal/orchestrator/orchestrator.go). Com 12 scanners disponíveis na v0.8.3 (trivy, grype, checkov, kics, dockle, kubescape, polaris, kube-score, terrascan, tfsec, regula e conftest), esse sinal é ainda mais importante: cada alvo aciona apenas o subconjunto que o suporta, e o status distingue "não se aplica" de "deveria rodar e falhou".
| Status | Significado | Como é determinado |
|---|---|---|
ran |
rodou e produziu resultado | a.Run retornou sem erro |
skipped |
não se aplica ao alvo | !a.Supports(target) |
unavailable |
binário ausente, probe estourou (timeout) ou foi morto (OOM) | falha no a.Version durante o probe |
error |
rodou mas falhou | a.Run retornou erro que não é deadline |
timeout |
excedeu --timeout por scanner |
runCtx.Err() == DeadlineExceeded |
O probe de versão (Options.ProbeTime, default 60s) é um diagnóstico de observabilidade por si só: ele distingue timeout de killed (OOM) de não instalado, e gera mensagens acionáveis (ex.: "raise the container's memory limit", "scope --scanners"). Ver também a seção de status de scanner em DESIGN.md §14.
2.3 Summary no terminal¶
Após emitir o relatório, printSummary (scan.go) escreve em stderr um resumo legível por humano (suprimido por --quiet). O summary é sempre em texto tabular — ele não é afetado por --log-format (o --log-format json cobre o stream de progresso logf, não o bloco de resumo):
── quorum summary ───────────────────────────
trivy ran 12 findings
grype ran 9 findings
checkov unavailable 0 findings (version probe killed — likely OOM …)
----------------------------------------
18 findings after consensus (7 multi-detected)
CRIT 1 HIGH 4 MED 9 LOW 3 INFO 1
elapsed 8.412s
note: 0 findings is not proof of safety — see scanner statuses above.
Sinais embutidos no summary: status + contagem por scanner, total pós-consenso, quantos foram multi-detectados (DetectionCount > 1), distribuição por severidade, e duração total (res.Duration). A nota final reforça o princípio anti-falso-negativo.
2.4 Métricas Prometheus via --metrics <arquivo> (implementado)¶
Fiel ao modelo efêmero, o Quorum não expõe um endpoint /metrics. Em vez disso, a flag --metrics <path> (off por default) faz o binário gravar um arquivo no formato texto do Prometheus ao fim da execução — pronto para ser coletado pelo node_exporter --collector.textfile ou empurrado ao Pushgateway no encerramento do job de CI. Assim a telemetria numérica existe sem servidor nem daemon.
O arquivo é escrito por writeMetricsFile (cmd/quorum/scan.go), que chama report.WriteMetrics (internal/report/metrics.go). Detalhes de implementação relevantes:
- Ordem de execução: o arquivo é gravado depois do relatório e do summary e antes do gate — portanto o
.promé produzido mesmo quando--fail-onvai dispararos.Exit(1). - Após gravar, um evento
metrics written to <path>é logado vialogf. - Permissões:
0644(métricas são contagens não sensíveis, pensadas para serem raspadas), diferentemente do relatório, que é0600por poder conter detalhe sensível de finding. O caminho passa porfilepath.Cleane os diretórios intermediários são criados comos.MkdirAll(0755). - Falha ao escrever o arquivo é fatal para a execução (
writing metrics: …), pois o operador pediu explicitamente a telemetria.
Exemplo de arquivo .prom gerado (rótulos e nomes exatamente como emitidos pelo código):
# HELP quorum_scan_duration_seconds Total scan wall-clock time in seconds.
# TYPE quorum_scan_duration_seconds gauge
quorum_scan_duration_seconds 8.412
# HELP quorum_scanner_up 1 if the scanner ran, else 0 (skipped/unavailable/error/timeout).
# TYPE quorum_scanner_up gauge
quorum_scanner_up{scanner="trivy",status="ran"} 1
quorum_scanner_up{scanner="checkov",status="unavailable"} 0
# HELP quorum_scanner_findings Raw findings reported per scanner (pre-consensus).
# TYPE quorum_scanner_findings gauge
quorum_scanner_findings{scanner="trivy"} 12
# HELP quorum_scanner_duration_seconds Per-scanner duration in seconds.
# TYPE quorum_scanner_duration_seconds gauge
quorum_scanner_duration_seconds{scanner="trivy"} 3.14
# HELP quorum_findings_after_consensus Findings remaining after consensus merge.
# TYPE quorum_findings_after_consensus gauge
quorum_findings_after_consensus 18
# HELP quorum_findings_total Consensus findings by severity.
# TYPE quorum_findings_total gauge
quorum_findings_total{severity="CRITICAL"} 1
quorum_findings_total{severity="HIGH"} 4
quorum_findings_total{severity="MEDIUM"} 9
quorum_findings_total{severity="LOW"} 3
quorum_findings_total{severity="INFO"} 1
# HELP quorum_multi_detected Consensus findings corroborated by more than one scanner.
# TYPE quorum_multi_detected gauge
quorum_multi_detected 7
Métricas emitidas por report.WriteMetrics (todas do tipo gauge), com a fonte exata no código:
| Métrica | Rótulos | Fonte no código |
|---|---|---|
quorum_scan_duration_seconds |
— | res.Duration.Seconds() |
quorum_scanner_up |
scanner, status |
1 se ScannerRun.Status == "ran", senão 0 (um ponto por scanner) |
quorum_scanner_findings |
scanner |
ScannerRun.Findings (findings brutos, pré-consenso) |
quorum_scanner_duration_seconds |
scanner |
ScannerRun.Duration.Seconds() |
quorum_findings_after_consensus |
— | len(res.Merged) |
quorum_findings_total |
severity |
contagem de res.Merged por Severity (rótulos CRITICAL/HIGH/MEDIUM/LOW/INFO; UNKNOWN não é emitido) |
quorum_multi_detected |
— | quantos res.Merged têm DetectionCount > 1 |
Observações de fidelidade importantes:
- O rótulo
severityusa os valores em maiúsculas demodel.Severity(CRITICAL,HIGH,MEDIUM,LOW,INFO), pois é a serialização literal do enum. - Há uma distinção deliberada entre
quorum_scanner_findings(bruto, por scanner, antes do consenso) equorum_findings_after_consensus/quorum_findings_total(depois do merge de consenso). Não confunda os dois ao montar dashboards. - A saúde de cada scanner é exposta como
quorum_scanner_up(booleano) mais o rótulo textualstatus, permitindo tantosum(quorum_scanner_up)quanto o detalhe do motivo (status="timeout"etc.). - Não há métrica dedicada de gate (ex.:
quorum_gate_triggered); o resultado do gate é observável pelo exit code (§2.5) e pelo evento de loggate: ….
2.5 Exit codes como sinal de máquina¶
Os exit codes são o sinal primário consumido por pipelines (contrato em scan.go e root.go):
| Código | Significado |
|---|---|
0 |
OK — execução concluída e nenhum finding atingiu --fail-on |
1 |
Gate — algum finding >= --fail-on disparou (os.Exit(1)) |
2 |
Erro de uso/runtime |
Esse é o "monitoramento" mais barato e confiável de um job: o pipeline decide passar/falhar lendo um inteiro. Ver 06-interfaces-cli-e-formatos.md para o detalhamento das flags de gate (--fail-on, --min-severity).
2.6 Relatório como artefato auditável¶
O relatório é a trilha de auditoria persistente de cada execução. Em SARIF (formato primário) e JSON, o status de cada scanner é embutido no próprio documento — não fica só no log volátil:
- SARIF (
internal/report/sarif.go): a lista de scanners (scannerSummary→name/status/version) é gravada emproperties.scannersdorun; cada resultado carregapartialFingerprints["quorum/v1"](=Fingerprint = sha256(correlationKey)), permitindo deduplicação e rastreio estável entre execuções e ferramentas a montante (ex.: GitHub Code Scanning). - JSON (
internal/report/json.go): incluiscanners(osScannerRun, comstatus,version,findings,durationMs,error) e os findings canônicos detalhados. - XML (
internal/report/xml.go): expõestatuspor scanner como atributo.
Como o relatório carrega status + versão + duração por scanner + fingerprint determinístico, ele é auto-suficiente para auditoria post-mortem, mesmo que os logs do runner já tenham expirado. Ver 06-interfaces-cli-e-formatos.md.
2.7 Métricas da camada de aconselhamento (só com --advice)¶
Quando a camada de aconselhamento opt-in roda (--advice), report.WriteMetrics recebe um *report.AdviceMetrics não-nulo e anexa um bloco quorum_advice_* ao mesmo arquivo .prom. Essas séries só aparecem quando --advice está ligado; sem ela as métricas de aconselhamento estão ausentes e toda a saída é byte-idêntica. Elas são estritamente observacionais — a camada de aconselhamento é somente de apresentação e nunca afeta correlationKey, fingerprint, confidence, severidade agregada ou o gate --fail-on.
quorum_advice_enrichedé emitida sempre que--adviceestá ligado (mesmo com a Fase 0/2 puramente determinística, sem modelo). Suas contagens derivam dos findings mesclados: um finding conta pararemediationse tiver umaRemediation, parareferencesse tiver qualquerReferences, e pararecommendationse tiver um anexo de IAAdvice.quorum_advice_providerequorum_advice_fixsão emitidas apenas quando um provedor de IA foi de fato usado (--advice-provider=localouremote, ou seja,Providernão é vazio nemnone). Só com a Fase 0/2 há apenasquorum_advice_enriched.quorum_advice_fixreporta o laço verify-the-fix:proposedé quantos patches o modelo sugeriu,verifiedé quantos sobreviveram ao re-scan (aplicados a uma cópia temporária, re-escaneados com o mesmo scanner, mantidos só se o finding desapareceu e o arquivo ainda faz parse). A razãoverified/proposedé a taxa de sucesso do verify-the-fix.--fixnunca aplica automaticamente e é recusado com--advice-provider=remote(faria upload de código-fonte).
Exemplo de bloco de aconselhamento anexado ao arquivo .prom (só com --advice --advice-provider=local --fix=suggest):
# HELP quorum_advice_enriched Findings that gained an advisory attachment, by kind.
# TYPE quorum_advice_enriched gauge
quorum_advice_enriched{kind="remediation"} 14
quorum_advice_enriched{kind="references"} 11
quorum_advice_enriched{kind="recommendation"} 9
# HELP quorum_advice_provider The AI recommendation provider used (1 = active).
# TYPE quorum_advice_provider gauge
quorum_advice_provider{provider="local"} 1
# HELP quorum_advice_fix AI suggested fixes by verify-the-fix stage.
# TYPE quorum_advice_fix gauge
quorum_advice_fix{stage="proposed"} 3
quorum_advice_fix{stage="verified"} 2
Métricas de aconselhamento emitidas por report.WriteMetrics (todas gauge), com a fonte exata no código:
| Métrica | Rótulos | Emitida quando | Fonte no código |
|---|---|---|---|
quorum_advice_enriched |
kind = remediation | references | recommendation |
--advice ligado |
contagem de res.Merged com uma Remediation / References não-vazia / um anexo Advice |
quorum_advice_provider |
provider = local | remote |
provedor de IA usado | constante 1 para o provedor ativo (AdviceMetrics.Provider) |
quorum_advice_fix |
stage = proposed | verified |
provedor de IA usado | AdviceMetrics.FixProposed / AdviceMetrics.FixVerified (a taxa do verify-the-fix) |
Observações de fidelidade:
- O bloco de aconselhamento é anexado depois de
quorum_multi_detected; ele não altera nenhuma das métricas do core acima dele. Um consumidor anterior à camada de aconselhamento continua funcionando sem mudanças. quorum_advice_provideré um único ponto (valor1) para o provedor de fato usado — é um marcador de presença, não uma contagem.- Todo anexo de IA é rotulado como "AI-generated, advisory only" no relatório; as métricas apenas contam quantos findings carregam cada tipo de anexo. Se o modelo estiver inacessível a execução degrada graciosamente — o relatório sai sem conselho de IA, os contadores ficam em
0, e o scan nunca falha por causa da camada de aconselhamento. - A recomendação de IA é reproduzível (
temperature=0+ um cache em disco chaveado porfingerprint+provider+model), então execuções repetidas sobre os mesmos findings produzem contagens de conselho estáveis.
2.8 Mapa dos sinais atuais¶
flowchart TD
Run[quorum scan] --> Orq[orchestrator.Run]
Orq -->|Logf text/json| Stderr["stderr: [quorum] … / NDJSON"]
Orq --> Runs["ScannerRun{status,version,findings,duration,error}"]
Runs --> Sum[printSummary → stderr]
Runs --> Met["--metrics → arquivo .prom (0644)"]
Runs --> Rep[relatório SARIF/JSON/XML]
Adv["--advice (opt-in)"] -.->|AdviceMetrics| Met
Adv -.-> Rep
Run --> Exit[exit code 0/1/2]
Met --> TF[textfile collector / Pushgateway]
Rep --> Art[artefato / Code Scanning]
Stderr --> CILogs[logs do runner de CI]
3. O que é N/A (e por quê)¶
Para um binário/contêiner efêmero que termina em segundos, os pilares abaixo não se aplicam ao runtime do Quorum. Declaramos N/A com justificativa técnica; onde já existe uma saída equivalente compatível com o modelo efêmero (ex.: --metrics textfile), isso é apontado.
| Pilar | Status | Justificativa técnica |
|---|---|---|
Endpoint de métricas raspável (/metrics HTTP scrape) |
N/A (runtime) — mas ver §2.4 | Não há processo persistente para expor /metrics nem para ser raspado; o ciclo de vida (segundos) é mais curto que um intervalo típico de scrape. A exportação numérica existe, porém, via --metrics <arquivo> no formato textfile (coletado por node_exporter/Pushgateway). |
| Tracing distribuído (OpenTelemetry traces) | N/A (runtime) | Execução local, single-process, fan-out em goroutines dentro do mesmo processo. Não há saltos de rede entre serviços para correlacionar. |
Endpoint de Health check (/healthz, readiness/liveness) |
N/A | Não há servidor para checar saúde. O análogo de "health" é o exit code, o status por scanner e a métrica quorum_scanner_up. list-scanners serve como verificação de capacidade. |
| Dashboards ao vivo (Grafana) | N/A (runtime) | Sem série temporal emitida em tempo real. A visualização de tendência é responsabilidade da plataforma que ingere o SARIF ou o textfile de métricas. |
| Alertas (Alertmanager/PagerDuty) | N/A (runtime) | O alerta nativo é o gate por exit code (--fail-on) interpretado pelo CI. Regras de alerta podem ser montadas sobre as métricas --metrics no Prometheus, mas não são internas ao Quorum. |
| Log aggregation própria (ELK/Loki) | N/A | O Quorum escreve em stderr (text ou json); a agregação é delegada ao runner. --log-format json facilita a ingestão, mas reimplementar shippers de log seria reinventar a plataforma hospedeira. |
| APM / profiling contínuo | N/A | Sem processo vivo para perfilar continuamente; profiling pontual é tarefa de desenvolvimento (go test -bench, pprof ad hoc), não de produção. |
Importante: N/A significa "não aplicável ao modelo de execução atual", não "impossível". A §2.4 já mostra como exportar métricas sem virar serviço; a §4 detalha as evoluções ainda não implementadas.
4. Propostas futuras (NÃO implementado)¶
Tudo nesta seção é proposta, claramente separada do estado atual. Nada abaixo existe no código hoje. São desenhos de evolução compatíveis com a natureza efêmera e CLI-first do Quorum (princípio: telemetria opt-in, sem daemon).
Graduaram para o estado atual (§2): os antigos
--log-format json(agora §2.1),--metrics <arquivo>(agora §2.4) e as métricas da camada de aconselhamento (agora §2.7). Foram implementados e por isso saíram desta lista.
4.1 OpenTelemetry para o pipeline (spans opt-in via OTLP)¶
Mesmo sendo single-process, o pipeline scan → normalize → resolve aliases → correlate → score → report tem fases bem definidas e o fan-out de scanners é naturalmente um conjunto de spans irmãos. Uma exportação opt-in via OTLP (ativada por env vars OTel padrão e --otel) permitiria correlacionar uma execução de Quorum com o trace do pipeline de CI que a invocou (via traceparent herdado).
- [ ] Flag
--otel+ honrarOTEL_EXPORTER_OTLP_ENDPOINT,OTEL_SERVICE_NAME,traceparent. - [ ] Span raiz
quorum.scan; spans filhos por scanner (runOne) e por fase do correlator. - [ ] Exportar no shutdown (flush síncrono — processo efêmero não pode confiar em batch assíncrono).
gantt
title Trace proposto de um quorum scan (spans)
dateFormat X
axisFormat %S
section scan
quorum.scan :a, 0, 9
section scanners (fan-out)
trivy.run :0, 4
grype.run :0, 5
checkov.probe (OOM) :0, 1
section pós
correlate+alias :5, 7
consensus.score :7, 8
report.write :8, 9
4.2 Dashboards de tendência via SARIF (sem servidor)¶
A análise de tendência (findings ao longo do tempo, MTTR, taxa de multi-detecção) deve ser delegada à plataforma que ingere o SARIF — tipicamente GitHub Code Scanning, que já oferece histórico, gráficos e alertas a partir dos partialFingerprints["quorum/v1"]. Alternativamente, as métricas do arquivo --metrics podem alimentar um Prometheus/Grafana externo. O Quorum não precisa de Grafana próprio.
- [ ] Documentar o fluxo
quorum scan -f sarif→upload-sarif→ Code Scanning como o caminho de tendência recomendado. - [ ] (Opcional) job de relatório que coleta os arquivos
--metricsde várias execuções e plota tendências no Prometheus. - [ ] Garantir estabilidade do fingerprint entre versões (já é
sha256(correlationKey)).
4.3 "Alertas" opt-in pós-execução¶
Manter o gate por exit code como mecanismo primário, e oferecer um hook de notificação opcional (ex.: --notify-webhook <url> enviando o summary JSON), executado apenas no fim do processo. Sem polling, sem daemon.
- [ ] Flag
--notify-webhook(off por default; respeita--offline). - [ ] Payload = summary estruturado (severidades, status por scanner, gate).
- [ ] Falha de envio é não-fatal (degradação graciosa, como o alias OSV).
4.4 Resumo das propostas¶
| Recurso | Flag/mecanismo | Compatível com efêmero? | Situação |
|---|---|---|---|
| Logs JSON | --log-format json |
Sim (stderr) | Implementado (§2.1) |
| Métricas textfile | --metrics <path> |
Sim (arquivo, sem servidor) | Implementado (§2.4) |
| Métricas de aconselhamento | --advice → quorum_advice_* |
Sim (mesmo textfile) | Implementado (§2.7) |
| OTel pipeline | --otel + env OTLP |
Sim (flush no shutdown) | Proposta (§4.1) |
| Tendência SARIF | Code Scanning / Prometheus | Sim (delegado) | Proposta (§4.2) |
| Webhook | --notify-webhook |
Sim (one-shot) | Proposta (§4.3) |
5. Como observar o Quorum hoje (guia prático)¶
Checklist para integrar a observabilidade existente num pipeline:
- [ ] Capture stdout e stderr separadamente. Direcione o relatório com
-o report.sarife deixe os logs[quorum]/NDJSON em stderr para o runner. - [ ] Escolha o formato de log conforme o consumidor:
--log-format textpara leitura humana,--log-format jsonquando um agregador (Loki/ELK/CloudWatch) for parsear o stream linha a linha. - [ ] Use
--fail-onpara que o exit code seja seu sinal de alerta primário (gate). - [ ] Ative
--metrics quorum.promse você tem umnode_exportertextfile collector ou Pushgateway — o arquivo.promé gravado mesmo quando o gate falha, então não perca a telemetria em builds vermelhos. - [ ] Se usar
--advice, observe as sériesquorum_advice_*:quorum_advice_enrichedpara a cobertura de remediation/references/recommendation, e — com um provedor de IA — a razãoverified/proposeddequorum_advice_fixcomo taxa de sucesso do verify-the-fix. Lembre que elas estão ausentes sem--advicee nunca afetam o gate. - [ ] Faça upload do relatório como artefato (e/ou ingira o SARIF no Code Scanning) — é sua trilha de auditoria durável.
- [ ] Inspecione o status por scanner (
quorum_scanner_up, coluna do summary,properties.scanners), não só a contagem de findings: umunavailable/timeout/errorsignifica cobertura reduzida, não ausência de risco. - [ ] Não confunda
quorum_scanner_findings(bruto) comquorum_findings_after_consensus/quorum_findings_total(pós-consenso) ao montar dashboards. - [ ] Não use
--quietem CI a menos que você já persista o relatório JSON/SARIF (e/ou o.prom) —--quietremove o summary e o progresso em ambos os formatos. - [ ] Trate exit code
2como falha de infraestrutura (uso/runtime), distinta do gate1. - [ ] Em ambientes com pouca memória, observe as mensagens de probe (OOM/timeout) e ajuste memória ou
--scanners.
Exemplo (GitHub Actions, conceitual):
- name: Quorum scan
run: >
quorum scan . -f sarif -o quorum.sarif
--fail-on high --log-format json --metrics quorum.prom
# exit 1 => gate; exit 2 => erro de runtime
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: quorum.sarif }
- name: Upload raw report + metrics (auditoria/telemetria)
if: always()
uses: actions/upload-artifact@v4
with: { name: quorum-report, path: "quorum.sarif\nquorum.prom" }
6. Referências cruzadas¶
- 06-interfaces-cli-e-formatos.md — flags (
--metrics,--log-format,--fail-on,--min-severity,--advice*,--fix), gating e exit codes. - 06-interfaces-cli-e-formatos.md — estrutura do SARIF,
partialFingerprints, scanners noproperties. - 13-ia.md — enquadramento honesto da camada de IA/aconselhamento (o core não tem IA; a camada de aconselhamento local/remota é opt-in e desligada por default).
DESIGN.md§14 (status de scanner) e §6 (matriz de correlação/consenso).- Código:
cmd/quorum/scan.go(logfcom--log-format,printSummary,writeMetricsFile, wiring de aconselhamento, exit codes),internal/report/metrics.go(WriteMetrics,AdviceMetrics),internal/orchestrator/orchestrator.go(ScannerRun, probe, status),internal/report/sarif.go(scannerSummary, fingerprints).
Premissas¶
- Versão de referência: v0.8.3 (revisão 2026-07-04); afirmações verificadas em
cmd/quorum/scan.go,internal/report/metrics.go,cmd/quorum/root.go,internal/orchestrator/orchestrator.goeinternal/report/sarif.gono estado atual do repositório. --log-format jsonproduz um envelope minimalista e estável por linha (ts,level,msg), comlevelhoje sempre"info"; não assumi campos tipados adicionais (scanner,event,durationMs) que não existem no struct atual.--metricsgrava um arquivo Prometheus textfile com permissão0644; os nomes e rótulos de métrica foram copiados literalmente deinternal/report/metrics.go(incluindoseverityem maiúsculas e a ausência de umquorum_gate_triggered). Assumi queUNKNOWNnão é emitido porque o loop de severidade emWriteMetricsnão o inclui.- As séries
quorum_advice_*são emitidas apenas quando--advicerodou (WriteMetricsrecebe um*AdviceMetricsnão-nulo);quorum_advice_providerequorum_advice_fixexigem um provedor de IA de fato (local/remote, nãonone). Nomes e rótulos (kind,provider,stage) foram copiados literalmente deinternal/report/metrics.go. Sem--advicea saída é byte-idêntica. - Assumi que o consumidor de telemetria é a plataforma invocadora (runner de CI,
docker run, shell) ou um coletor externo (textfile/Pushgateway), pois o Quorum não possui stack de observabilidade próprio. - O nome dos campos no SARIF (
properties.scanners,partialFingerprints["quorum/v1"]) e no JSON (scanners,durationMs) foi tirado do código; mudanças futuras de schema podem alterá-los. - As flags restantes na §4 (
--otel,--notify-webhook) são hipotéticas; nenhuma existe hoje e os nomes são sugestões de design, não compromissos. - Os exemplos de saída (summary,
.prom, NDJSON, bloco de aconselhamento) são ilustrativos; valores numéricos não vêm de uma execução real. - O default do probe de versão é 60s (
defaultProbeTime) e o--timeoutpor scanner é 5m, conforme o código; assumi que esses são os defaults efetivos quandoOptions.ProbeTime/PerScannerTimenão são sobrescritos.