Backend¶
O "backend" do Quorum (v0.8.3) é o conjunto de pacotes Go que compõem a CLI/Docker.
Não há servidor de aplicação, processo de longa duração (daemon) nem API HTTP: o backend
é uma biblioteca de pacotes internal/* orquestrada por um binário de linha de comando (cmd/quorum).
Cada execução de quorum scan é um processo efêmero que faz fan-out para 12 scanners externos (trivy,
grype, checkov, kics, dockle, kubescape, polaris, kube-score, terrascan, tfsec, regula, conftest),
normaliza tudo no modelo canônico model.Finding, resolve aliases de vulnerabilidades,
correlaciona findings equivalentes, calcula um score de consenso e emite um relatório SARIF/JSON/XML
(opcionalmente também métricas Prometheus). O princípio de projeto que permeia todo o código é
"false split > false merge" (preferir jamais fundir dois findings distintos a fundir
incorretamente dois findings diferentes) e "0 findings não é prova de segurança" (a ausência de
resultados nunca equivale a segurança comprovada).
Desde a v0.8.3 existe também uma camada de advisory opt-in (--advice): um conjunto de pacotes
estritamente apresentacionais (internal/enrich, internal/rag, internal/advisor) que anexam
templates de remediação, referências OWASP e — opcionalmente — uma recomendação gerada por IA.
Ela é desligada por padrão e nunca toca em correlationKey / fingerprint / confidence /
severidade agregada / gate --fail-on; sem --advice a saída é byte-a-byte idêntica. O núcleo
determinístico continua sem IA.
Este documento mapeia o template corporativo de "Backend" (Layers, Services, Repositories, Controllers, Middlewares, Workers, Jobs, Cache, Queue/Messaging) sobre a realidade do código. Quando o template pede um conceito típico de aplicações web/distribuídas que não existe neste produto, o item é marcado como N/A com uma justificativa técnica.
Documentos relacionados: Arquitetura · Modelo de dados · CLI / Comandos · Enquadramento de IA · este arquivo é a referência canônica do código Go.
Revisão: 2026-07-04 · versão de referência: v0.8.3.
1. Visão geral em camadas¶
O backend é estritamente em camadas, com dependências apontando sempre para dentro (do binário CLI
em direção ao domínio). Nenhum pacote de domínio importa cmd/quorum, e o pacote model não importa
ninguém — é o núcleo estável compartilhado por todos.
flowchart TD
subgraph CLI["Camada de Entrada (Controllers) — cmd/quorum"]
MAIN[main.go]
ROOT[root.go<br/>root cobra + list-scanners + advise-index]
SCAN[scan.go<br/>comando scan + guardas de DoS]
end
subgraph APP["Camada de Aplicação / Orquestração"]
ORCH[orchestrator<br/>fan-out + status por scanner]
end
subgraph SVC["Camada de Serviços de Domínio"]
CORR[correlate<br/>identidade + chave]
CONS[consensus<br/>agrupamento + score]
ALIAS[alias<br/>resolução CVE/GHSA]
CW[crosswalk<br/>regra -> controle canônico]
FILT[filter<br/>baseline + min-severity]
REP[report<br/>SARIF/JSON/XML + métricas]
end
subgraph ADV["Camada de Advisory (opt-in, apenas apresentação) — --advice"]
ENR[enrich<br/>Fase 0: templates de remediação + refs OWASP]
RAG[rag<br/>Fase 2: corpus OWASP com digest fixado]
ADVI[advisor<br/>Fase 1/3: LLM local/remoto + verify-the-fix]
end
subgraph GW["Camada de Gateways (Repositories)"]
AD[adapter<br/>wrappers das 12 CLIs de scanner]
CACHE[cache<br/>KV JSON em arquivo]
OSV[alias/osv<br/>cliente HTTP OSV.dev]
end
subgraph CORE["Core / utilitários puros"]
MODEL[model<br/>Finding / MergedFinding]
SEV[severity<br/>tabela de normalização]
PURL[purl<br/>identidade de pacote]
end
EXT[(scanners OSS<br/>trivy/grype/checkov/kics/<br/>dockle/kubescape/polaris/<br/>kube-score/terrascan/tfsec/<br/>regula/conftest)]
NET[(API OSV.dev)]
AIEP[(endpoint compatível<br/>com OpenAI: Ollama/remoto)]
FS[(Sistema de arquivos:<br/>cache, crosswalk YAML,<br/>knowledge pack, corpus OWASP,<br/>política Rego, .quorumignore,<br/>output, métricas)]
MAIN --> ROOT --> SCAN
SCAN --> ORCH
SCAN --> CORR & FILT & REP & CW & ALIAS & CACHE
SCAN -.opt-in.-> ENR & RAG & ADVI
ORCH --> AD
ORCH --> CORR
ORCH --> CONS
CORR --> ALIAS & CW
ALIAS --> CACHE & OSV
AD --> EXT
OSV --> NET
ADVI --> AIEP
RAG --> AIEP
CACHE --> FS
CW --> FS
ENR & RAG --> FS
REP --> FS
SCAN -.model compartilhado.-> MODEL
CORR & CONS & FILT & REP & AD --> MODEL
ENR & RAG & ADVI --> MODEL
AD & CONS --> SEV
AD & CORR --> PURL
Tabela de camadas¶
| Camada | Pacote(s) | Responsabilidade | Depende de |
|---|---|---|---|
| Entrada (Controllers) | cmd/quorum |
Parsing de flags (cobra), validação/inferência de alvo, guardas de DoS (tamanho do alvo, injeção de argumentos), wiring de dependências, códigos de saída | toda a camada de aplicação/serviços |
| Aplicação / Orquestração | internal/orchestrator |
Selecionar adapters, fan-out paralelo, timeout/probe, coletar findings, disparar enrich+consensus | adapter, correlate, consensus, model |
| Serviços de domínio | correlate, consensus, alias, crosswalk, filter, report |
Lógica de negócio: identidade, score, resolução de alias/controle, supressão, serialização (report + métricas) | model, severity, purl, cache, osv |
| Advisory (opt-in) | enrich, rag, advisor |
Apenas apresentação: templates de remediação + referências OWASP + recomendação/fix opcional por LLM. Nunca afeta o gating | model, cache; endpoint compatível com OpenAI opcional |
| Gateways (Repositories) | adapter, cache, alias/osv |
Acesso a recursos externos: processos CLI, arquivo de cache, HTTP OSV | model, severity, purl |
| Core / utilitários | model, severity, purl |
Tipos canônicos e funções puras, sem efeitos colaterais | nada (model é folha) |
Regra de ouro do repo (
internal/model/model.go): "Nada no pipeline opera sobre o JSON bruto de um scanner — todo adapter normaliza para Finding, e toda etapa posterior fala apenas Finding e MergedFinding." ORaw map[string]anyé mantido mas marcadojson:"-"(não serializado por padrão).
2. Pipeline de execução (do comando ao relatório)¶
O caminho completo de uma execução de scan, com os pacotes responsáveis por cada estágio:
sequenceDiagram
participant U as Usuário/CI
participant C as cmd/quorum (scan.go)
participant O as orchestrator
participant A as adapters (goroutines)
participant E as correlate (Correlator)
participant K as consensus
participant F as filter
participant V as advisory (enrich/rag/advisor)
participant R as report
U->>C: quorum scan <alvo> [flags]
C->>C: validateTargetRef / resolveTargetType / checkTargetSize
C->>C: Parse(fail-on,min-sev) / valida log-format / valida flags de advice
C->>C: LoadBaseline / crosswalk.Load / cache.Open
C->>C: correlate.New(alias.New(cache,osv), crosswalk)
C->>O: orchestrator.Run(ctx, target, Options)
par fan-out paralelo
O->>A: probe Version() (timeout 60s)
A-->>O: ran|skipped|unavailable|error|timeout
O->>A: Run() (timeout = --timeout, saída limitada)
A-->>O: []model.Finding
end
O->>E: Enrich(ctx, allFindings)
E->>E: resolveVuln (alias) / resolveControl (crosswalk)
E->>E: BuildKey + Fingerprint (sha256)
O->>K: consensus.Merge(allFindings)
K-->>O: []MergedFinding (score + ordenação)
O-->>C: Result{Runs, Findings, Merged}
C->>F: filter.Apply(min-severity, baseline)
opt --advice (apenas apresentação, após fixadas as entradas de gating)
C->>V: enrich.Enrich / rag.AttachReferences / advisor.Enrich
V-->>C: Remediation / References / Advice anexados in place
end
C->>R: report.Write(SARIF|JSON|XML) [+ WriteMetrics]
C->>U: relatório + resumo + código de saída (0/1/2)
Estágios em prosa:
- Controller (
scan.go) valida flags, rejeita alvos maliciosos (-inicial), aplica o limite de tamanho do alvo, infere o tipo do alvo, carrega baseline/crosswalk/cache e injeta as dependências noCorrelator. Também valida as flags de advisory (--advice-provider,--fix, consentimento de egress) antes de qualquer trabalho. - Orchestrator seleciona os adapters que suportam o alvo, roda cada um em uma goroutine, faz o
version probe e a execução com timeout (e limite de stdout) e coleta os
Findings canônicos. - Correlate enriquece (resolve aliases para VULN, controles para MISCONFIG/K8S_POSTURE/
IMG_HARDENING) e carimba
CorrelationKey+Fingerprint. - Consensus agrupa por
CorrelationKeye calculaConfidence/DetectionCount. - Filter aplica
--min-severitye a baseline.quorumignore. - Advisory (apenas se
--advice) roda depois do consensus e com as entradas de gating fixadas: anexa templates de remediação, referências OWASP e opcionalmente uma recomendação/fix de IA — estritamente apresentacional, jamais mutando a chave/fingerprint/confidence/severidade/gate. - Report serializa o relatório e, se
--metricsfoi passada, escreve as métricas Prometheus (incluindo as séries de advisory quando--adviceestá ligado); o controller decide o código de saída via--fail-on.
3. Services (lógica de domínio)¶
Os "services" do template correspondem aos pacotes de domínio. Nenhum deles mantém estado global mutável de longa vida; são funções/objetos instanciados por execução.
3.1 internal/orchestrator — serviço de orquestração¶
Arquivo-chave: orchestrator.go. Expõe Run(ctx, target, Options) (*Result, error).
Optionscarrega:Scanners []string,PerScannerTime(timeout por scanner),ProbeTime(timeout do version-probe, padrão60sviadefaultProbeTime),CorrelatoreLogf.ResultagregaRuns []ScannerRun,Findings []model.Finding(bruto, para detalhe JSON),Merged []model.MergedFinding,Duration.ScannerRun.Statusassumeran | skipped | unavailable | error | timeout— a transparência de status é deliberada: "0 vulns nunca pode parecer que o scan não rodou".runOnedistingue as causas de falha do probe: timeout (DeadlineExceeded), morto por sinal/OOM (killedSignaldetecta"signal: killed") e binário ausente, emitindo mensagens de erro acionáveis (ex.: "aumente o limite de memória do container").- Quando
Correlator == nil, o orchestrator ainda computaCorrelationKey/Fingerprintviacorrelate.BuildKey/Fingerprintpara que o agrupamento de consensus funcione mesmo sem enriquecimento.
3.2 internal/correlate — serviço de identidade¶
Arquivos: correlate.go (enriquecimento) e key.go (chave determinística).
Correlator{Alias, Crosswalk}— ambas as dependências podem sernil(degrada para id-como-está / não-mapeado).Enrichitera sobre os findings:TypeVuln→resolveVuln(alias);TypeMisconfig/TypeK8sPosture/TypeImgHardening→resolveControl(crosswalk). Depois aplicaBuildKey+Fingerprint.BuildKeyé pura e determinística, com uma chave por tipo (não há chave universal):
| Tipo | Formato da CorrelationKey |
|---|---|
VULN |
VULN\|<VULNID em maiúsculas>\|<name@version do PURL> |
MISCONFIG |
MISCONFIG\|<basename do arquivo>\|<tipo de recurso>\|<controle> |
K8S_POSTURE |
K8S\|<ns/kind/name>\|<controle> |
IMG_HARDENING |
IMGH\|<controle> |
SECRET |
SECRET\|<caminho normalizado>\|<linha>\|<ruleId> |
| outros | OTHER\|<scanner>\|<título> |
- Nota de granularidade cross-engine: a chave
MISCONFIGusa basename + tipo de recurso + controle (engines discordam sobre caminho e identidade de recurso); a chaveK8S_POSTUREusa objeto (ns/kind/name) + controle, deliberadamente sem o container, pois kubescape reporta no nível do workload e polaris por container — incluir o container bloquearia o consenso cross-engine (documentado emkey.go). Fingerprint(key) = sha256(key)em hex — é opartialFingerprints["quorum/v1"]no SARIF.controlKeyprefere o controle canônico resolvido; quando não-mapeado, usaUNMAPPED:<scanner>:<ruleId>para jamais fundir silenciosamente findings distintos.
3.3 internal/consensus — serviço de score¶
Arquivo: consensus.go. Expõe Merge([]Finding) []MergedFinding.
- Agrupa por
CorrelationKey, preservando a ordem de primeira aparição. - Para cada grupo computa:
DetectedBy(scanners distintos),Severity(máximo agregado),DetectionCount,Unmapped(qualquer membro),Confidence. - Fórmula de confiança (DESIGN §9), pesos: count
0.35, diversidade0.25, severidade0.25, autoritativo0.15. - count:
log(1+n)/log(5)— retornos decrescentes. - diversidade: famílias de engine distintas (
scannerCategory: sca, iac, k8s, hardening, policy); 1 família ≈ 0.33, 2 ≈ 0.66, 3+ = 1.0 — dois engines diferentes valem mais que dois iguais. - autoritativo: 1.0 se
Confirmedou se for um CVE com CVSS > 0. - Ordena por
(severity, confidence, detectionCount, correlationKey)decrescente para saída estável e útil.
Mapa atual de scannerCategory (12 scanners → 5 famílias):
| Família | Scanners |
|---|---|
sca |
trivy, grype |
iac |
checkov, kics, terrascan, tfsec, regula |
k8s |
kubescape, polaris, kube-score |
hardening |
dockle |
policy |
conftest |
3.4 internal/alias — serviço de resolução de identificadores¶
Arquivos: resolver.go (cadeia) e osv.go (cliente OSV.dev).
chainResolverresolve um id de vuln para a forma canônica, preferindo CVE, em 3 camadas:- aliases já presentes no finding (
preferCVE), - cache local (
cache.Store), - arbitragem OSV.dev (
osvSource). - Nunca retorna erro: degrada graciosamente para o melhor id disponível em falha de rede.
--offlinepassaosv = nil, desabilitando a camada 3 (usa apenas aliases locais + cache).OSVClienttem retry com backoff exponencial, timeout HTTP e classifica falhas como retryable (rede, 429, 5xx) ou não.
3.5 internal/crosswalk — serviço de mapeamento de controles¶
Arquivo: crosswalk.go. Carrega *.yaml/*.yml de um diretório e indexa
"scanner|ruleID" → Resolution{Control, Category, CWE, Title}.
- Um diretório ausente não é erro (roda sem crosswalk customizado).
Resolveretornaok=falsequando não há mapeamento — o chamador mantém o finding isolado e o marcaUnmapped("nunca adivinhe um match", DESIGN §6).- O diretório padrão é
./crosswalkcom fallback automático para/opt/quorum/crosswalk(o bundle da imagem Docker), viaresolveCrosswalkDiremscan.go. - Consenso habilitado além de SCA: os mapeamentos são derivados da saída real dos scanners (aplicando "false split > false merge"). O bundle inclui:
crosswalk/aws.yaml,azure.yaml,gcp.yaml— hub AVD; cobre S3/IAM/EBS/SG/RDS/KMS/ CloudTrail/VPC-flow-logs (AWS), Storage/Key Vault (Azure), bucket/firewall/SQL (GCP).crosswalk/k8s.yaml— hub kubescape C-####; correlaciona kubescape × polaris × kube-score em controles como privilege-escalation, privileged, non-root, cpu/mem limits, probes, read-only-fs, linux-hardening, service-account automount, network-policy, host-network, host-PID/IPC, capabilities e secrets.- tfsec auto-correlaciona com trivy sem crosswalk: emite AVD nativo (extrai
AVD-<PROV>-<n>do link e armazena emCanonicalControl). Vertfsec.go. - RBAC permanece single-engine (o RBAC do kubescape requer contexto de cluster; documentado como limitação, não fundido com outros engines).
3.6 internal/filter — serviço de pós-processamento/gating¶
Arquivo: filter.go. Aplica o corte de severidade mínima e a baseline antes do report/gating.
Baselinecasa porFingerprintOUCorrelationKey(o usuário pode copiar qualquer um do relatório); o arquivo.quorumignore(padrão), comentários#, linhas em branco ignoradas.LoadBaselinedistingue "ausente" (ok=false) de "presente porém vazio" — o controller exige existência apenas quando--baselinefoi passado explicitamente.ApplyretornaResult{Kept, SuppressedBaseline, SuppressedSeverity}— ele sempre loga supressões (um finding suprimido continua sendo um finding, DESIGN §14).
3.7 internal/report — serviço de serialização¶
Arquivos: report.go (dispatch de formato), sarif.go, json.go, xml.go, metrics.go.
Write(w, res, format)faz dispatch para SARIF (primário), JSON ou XML.ParseFormatvalida--format.- SARIF carrega
partialFingerprints["quorum/v1"]derivado deFingerprint. WriteMetrics(w, res, adv)(metrics.go) emite o resultado em formato texto Prometheus, adequado ao textfile collector do node_exporter ou a um Pushgateway — telemetria exportável para uma CLI sem processo de longa duração para scrape. Séries emitidas:quorum_scan_duration_seconds,quorum_scanner_up{scanner,status},quorum_scanner_findings{scanner},quorum_scanner_duration_seconds{scanner},quorum_findings_after_consensus,quorum_findings_total{severity}equorum_multi_detected. Quando--adviceestá ligado, também emite as séries de advisory (ver §3.8 e §5.1). Disparado pela flag--metrics <arquivo>(ver §5.1).
3.8 Serviços de advisory (internal/enrich, internal/rag, internal/advisor) — opt-in¶
A camada de advisory é apenas apresentação e desligada por padrão. Ela roda após o consensus e
com as entradas de gating fixadas, anexando campos ao MergedFinding in place sem jamais tocar em
CorrelationKey / Fingerprint / Confidence / severidade agregada / gate --fail-on.
Sem --advice, o relatório é byte-a-byte idêntico. O núcleo determinístico não tem IA; as partes
de IA (Fases 1 e 3) são estritamente opt-in e desligadas por padrão. Toda anexação de IA é rotulada
"AI-generated, advisory only". Ver Enquadramento de IA e a proposta de IA.
3.8.1 internal/enrich — Fase 0 (determinística, sem modelo)¶
Arquivo: enrich.go. Load(dir) lê o knowledge pack curado (knowledge/*.yaml:
aws/azure/gcp/k8s/image/categories) para um KB; KB.Enrich(merged) anexa uma
model.Remediation (template de correção curado) mais referências OWASP model.DocRef, casadas por
canonicalControl / ruleId / category / type. Sem modelo, totalmente determinística. KB.Len()
reporta quantas entradas foram carregadas. O diretório padrão é ./knowledge com fallback para o
/opt/quorum/knowledge embutido na imagem (via resolveKnowledgeDir), configurável com --knowledge.
3.8.2 internal/rag — Fase 2 (RAG-como-artefato, determinística)¶
Arquivos: rag.go, lexical.go, embed.go. Recuperação de um corpus OWASP versionado e com digest
fixado (knowledge/owasp/corpus.yaml). Load(dir) lê o corpus e verifica seu
ContentDigest; um corpus adulterado falha o pin e é recusado. A recuperação é léxica por
padrão (sem modelo) via NewLexical; semântica (embeddings, NewSemantic) apenas quando o corpus
traz vetores e um endpoint local está configurado. O scan escolhe semântica automaticamente quando
corpus.HasEmbeddings() e --advice-provider=local. AttachReferences(merged, retriever, k)
anexa as top-k passagens OWASP como model.DocRefs; GroundingText produz a string de grounding
que alimenta o prompt da Fase 1. Os embeddings são excluídos do content digest, de modo que o
pin é preservado após quorum advise-index.
3.8.3 internal/advisor — Fase 1 (LLM local) e Fase 3 (provider remoto), opt-in¶
Arquivos: advisor.go, client.go, prompt.go, verify.go. Advisor.Enrich(ctx, merged) consulta um
endpoint compatível com OpenAI por uma recomendação em linguagem natural, anexando um model.Advice e
retornando Stats{Advised, FixProposed, FixVerified}. Ele nunca retorna um erro que faz o scan
falhar: na primeira falha de conectividade ele loga uma vez e para (degradação graciosa — o relatório
sai sem advice de IA e o scan nunca falha).
- Fase 1, local (
NewLocalClient,--advice-provider=local): consulta um endpoint on-host (ex.: Ollama). Reproduzível viatemperature=0mais um cache em disco chaveado por fingerprint+provider+model. --fix=suggest: propõe um patch que precisa passar por um re-scan verify-the-fix (verify.go): aplica a uma cópia temporária, re-escaneia com o mesmo scanner (via a interfaceRescanner, apoiada no registry de adapters emcmd/quorum/advisor.go) e mantém o patch apenas se o finding sumiu e o arquivo ainda parseia. Ele nunca aplica automaticamente. Fixes são restritos a alvos repo/k8s (não a alvos de imagem).- Fase 3, remoto (
NewRemoteClient,--advice-provider=remote): chama uma API externa (auth viaQUORUM_ADVICE_API_KEY). Dados saem do host, então é condicionado a consentimento explícito (--advice-allow-egress), bloqueado por--offlinee recusa--fix(que faria upload de código-fonte). Apenas o finding normalizado é enviado — jamais código-fonte.
4. Repositories (gateways de acesso a recursos externos)¶
O Quorum não tem repositório de banco relacional. O papel de "repository" (uma abstração sobre acesso a um recurso externo persistente ou de I/O) é desempenhado por três gateways:
| Gateway | Pacote | Recurso externo | Padrão |
|---|---|---|---|
| Adapters de scanner | internal/adapter |
Processos CLI (12 scanners) | Interface + Registry + exec.CommandContext |
| Cache de alias | internal/cache |
Arquivo JSON em disco | KV store com flush atômico |
| Cliente OSV | internal/alias/osv.go |
API HTTP OSV.dev | Cliente HTTP com retry/backoff |
O cache de alias também é reutilizado (com um caminho padrão distinto) pela camada de advisory como o cache de advice de IA — ver §3.8.3 e §9.
4.1 Adapters como gateways de scanner¶
O pacote adapter é o gateway para o "mundo externo" dos scanners. Cada adapter implementa a
interface Adapter:
type Adapter interface {
Name() string
Version(ctx context.Context) (string, error)
Supports(target Target) bool
Capabilities() []Capability
Run(ctx context.Context, target Target) ([]model.Finding, error)
}
- Registry: cada adapter chama
Register(&xxx{})no seuinit();Get/All/Namesexpõem o registry. Um nome duplicado causapanic(erro de programação). - Execução de processo:
runCmdroda o binário, dobra o stderr no erro e trata exit não-zero com stdout não-vazio como sucesso (vários scanners saem != 0 justamente porque encontraram problemas).toolVersionfaz o version probe e detecta binário ausente. - Normalização: cada adapter tem seu próprio parser (ex.:
trivy.parse) que traduz o JSON nativo em[]model.Finding, usandoseverity.FromLabel/FromCVSS/FromDockleepurl.Build. Adapters nunca computamCorrelationKey— isso é centralizado no correlator. - Testes de contrato: cada adapter tem um teste de contrato contra fixtures em
internal/adapter/testdata(veradapter_test.go,realdata_test.go).
Adapters registrados (12): trivy, grype (família sca); checkov, kics, terrascan, tfsec,
regula (iac / MISCONFIG); kubescape, polaris, kube-score (k8s / K8S_POSTURE);
dockle (hardening / IMG_HARDENING); conftest (policy — policy-as-code).
Notas por adapter relevantes ao backend:
conftestnão tem regras embutidas: avalia seu próprio Rego (em./policypor padrão, ou viaQUORUM_CONFTEST_ARGS="--policy <dir>"). Sem políticas ele dá erro e o scanner é reportado comoerror— esperado, já que policy-as-code é opt-in.tfsecderiva o AVD do link e armazena emCanonicalControl, correlacionando com trivy sem crosswalk.kubescapeescreve o relatório em um arquivo temporário (--output) e trata exit não-zero com relatório válido como sucesso.
Helpers compartilhados do pacote (adapter.go)¶
| Helper | Papel |
|---|---|
maxOutputBytes() / capWriter |
Limite de DoS no stdout do scanner. runCmd bufferiza em capWriter (padrão 512 MiB, defaultMaxOutputBytes, override via QUORUM_MAX_OUTPUT_BYTES); se excedido, aborta com erro em vez de OOM. capWriter ainda retorna len(p) para que o pipe do processo filho nunca bloqueie, marcando over e detectando truncamento após a execução. |
extraArgs(name) / splitArgs(s) |
Passthrough de argumentos por scanner via env QUORUM_<NAME>_ARGS (ex.: QUORUM_CHECKOV_ARGS="--bc-api-key <key>" destrava políticas Prisma Cloud/Bridgecrew; QUORUM_CONFTEST_ARGS="--policy <dir>"). splitArgs faz um split ao estilo shell (respeita aspas simples/duplas, sem expansão de variável). O valor é controlado pelo operador — mesmo nível de confiança das flags. Aplicado por todos os adapters. |
redactSecretText(s) |
Redação de segredo: mascara tokens longos ([A-Za-z0-9+/=_-]{12,}), preservando os 4 primeiros chars + …REDACTED…. Usado pelo trivy para carregar o contexto (linha) de um SECRET sem vazar o valor ("matched (redacted): " + redactSecretText(s.Match)). |
runCmd / toolVersion |
Execução de processo e version probe (descritos acima). |
4.2 Cache como repository¶
internal/cache/store.go é um KV map[string]string persistido em arquivo JSON — uma escolha
deliberada para dar à resolução de alias idempotência/velocidade sem trazer um banco CGO.
Open(path)carrega; um arquivo ausente/corrompido vira um cache vazio (nunca quebra um scan).Putescreve com rename atômico (.tmp→ destino, perm0600) e cria diretórios pai sob demanda; erros de flush são engolidos de propósito (o cache é uma otimização, não uma fonte de falha). O arquivo carrega umschemaVersionpara invalidação de formato.- Seguro para uso concorrente (
sync.RWMutex) — relevante porque os adapters rodam em paralelo e compartilham o resolver. - Caminho padrão:
~/.cache/quorum/aliases.json(viaos.UserCacheDir), configurável com--cache. - O mesmo tipo de store apoia o cache de advice de IA (
--advice-cache, padrão~/.cache/quorum/advice.json), chaveado por fingerprint+provider+model para execuções de CI reproduzíveis.
4.3 Cliente OSV como repository remoto¶
internal/alias/osv.go é o gateway HTTP para o OSV.dev (GET /v1/vulns/<id>), o "árbitro de última
instância" na cadeia de alias. O id é validado e passa por url.PathEscape antes da
requisição. Detalhes em §3.4.
5. Controllers (comandos cobra)¶
A camada de entrada (cmd/quorum) é o "controller" do Quorum: traduz argumentos/flags em chamadas de
serviço, faz o wiring de dependências e define códigos de saída.
| Arquivo | Papel |
|---|---|
main.go |
Ponto de entrada; roda o root, imprime o erro e sai com exit 2 em falha de uso/runtime |
root.go |
Define o comando root quorum (cobra) e os subcomandos list-scanners + advise-index |
scan.go |
Define o comando scan, valida/protege o alvo, faz o wiring de todas as dependências e roda o pipeline |
advisor.go |
Faz o wiring da camada de advisory para o scan: o adapterRescanner (re-scan de verify-the-fix via o registry de adapters) e o caminho do cache de advice de IA |
advise_index.go |
Define o comando advise-index (embute o corpus OWASP, preservando o pin de digest) |
5.1 Comandos¶
scan <alvo>(ExactArgs(1)): o comando principal. Flags centrais:--type(image|repo|k8s, inferido se omitido),--scanners,--format/-f(sarif|json|xml),--output/-o,--fail-on,--min-severity,--baseline(.quorumignore),--crosswalk(./crosswalk+ fallback/opt/quorum/crosswalk),--cache,--metrics <arquivo>(escreve métricas Prometheus em textfile),--log-format(text|json, formato do log de progresso em stderr),--timeout(padrão5m),--offline,--quiet/-q.
Flags de advisory (todas opt-in; sem --advice a saída é byte-a-byte idêntica):
| Flag | Padrão | Significado |
|---|---|---|
--advice |
false |
Anexa templates de remediação determinísticos + referências OWASP (advisory; não afeta o gating) |
--advice-provider |
none |
Provider de recomendação por IA: none|local|remote |
--advice-endpoint |
http://localhost:11434/v1 |
Base URL compatível com OpenAI (local por padrão; defina a URL remota para remote) |
--advice-model |
qwen2.5-coder:7b |
Id do modelo para --advice-provider=local |
--advice-embed-model |
nomic-embed-text |
Modelo de embedding para recuperação semântica OWASP (apenas quando o corpus traz embeddings) |
--advice-cache |
~/.cache/quorum/advice.json |
Arquivo de cache de advice de IA (chaveado por fingerprint+model) |
--advice-max |
50 |
Máximo de findings enviados ao provider de IA por execução (0 = sem limite) |
--advice-allow-egress |
false |
Consentimento para enviar findings para fora do host; obrigatório para --advice-provider=remote |
--fix |
off |
Modo de fix sugerido por IA: off|suggest (suggest emite um patch verify-the-fix; nunca aplica automaticamente) |
--knowledge |
./knowledge |
Diretório de arquivos do knowledge pack de advisory (templates de remediação + refs OWASP) |
Validação de flags de advisory (em scan.go): --advice-provider deve ser none|local|remote e
--fix deve ser off|suggest; --advice-provider=remote é bloqueado por --offline, exige
--advice-allow-egress e QUORUM_ADVICE_API_KEY, e recusa --fix (ele faria upload de
código-fonte).
advise-index(NoArgs): embute o corpus RAG OWASP, transformando a recuperação léxica padrão em semântica. Ele lê o corpus, embute cada chunk via um endpoint de embeddings local compatível com OpenAI (ex.: Ollama) e o escreve de volta com vetores por chunk. Os embeddings são excluídos do content digest, então o pin é preservado. Flags:--corpus,--out,--advice-endpoint,--advice-embed-model.list-scanners: lista os adapters registrados e suas capabilities (Capabilities().Type).
Além das flags, o comportamento é ajustável via variáveis de ambiente (nível de confiança do
operador): QUORUM_<NAME>_ARGS (passthrough por scanner, §4.1), QUORUM_MAX_OUTPUT_BYTES (limite de
stdout, padrão 512 MiB), QUORUM_MAX_TARGET_BYTES (limite de tamanho do alvo em disco, padrão 20 GiB;
0 desabilita) e QUORUM_ADVICE_API_KEY (auth para --advice-provider=remote).
5.2 Inferência e validação de alvo¶
validateTargetRef: recusa um alvo começando com-(evita injeção de argumentos em um scanner downstream; direcione./-namepara um caminho literal).checkTargetSize: para alvosrepo/k8s, faz um walk que para assim que o limite (QUORUM_MAX_TARGET_BYTES, padrão 20 GiB) é ultrapassado; alvosimagesão pulados.resolveTargetType: se--typefor omitido, um caminho em disco existente →repo; caso contrário →image. Aceita aliases (fs/dir→ repo;kubernetes/manifests→ k8s).
5.3 Códigos de saída (o contrato de gating)¶
| Exit | Significado | Origem no código |
|---|---|---|
0 |
OK — nenhum finding atingiu --fail-on |
retorno normal de runScan |
1 |
Gate disparado — um finding ≥ --fail-on |
os.Exit(1) em runScan |
2 |
Erro de uso/runtime | os.Exit(2) em main.go |
A camada de advisory nunca altera o código de saída: um scan com
--adviceproduz exatamente o mesmo resultado de gate que sem ela.
5.4 Saída de progresso, resumo e artefatos¶
- O controller injeta um
Logfque escreve em stderr (silenciável via--quiet) em dois formatos:text(prefixo[quorum]) oujson(linha{ts,level,msg}), conforme--log-format. Sob--adviceele também loga os estágios de advisory (advice: knowledge=…,advice(rag): corpus=…,advice(ai): provider=… fixes: N verified / M proposed). - Ele imprime um resumo humano (status por scanner, contagens por severidade, multi-detected, tempo) — sempre terminando com "0 findings não é prova de segurança".
--outputé normalizado comfilepath.Cleane escrito com perm0600(pode carregar detalhe sensível de finding). O arquivo--metricsé escrito com0644(contagens não-sensíveis, destinadas a scrape).
6. Middlewares — N/A (com equivalentes)¶
N/A. Não há cadeia de middleware HTTP nem framework de interceptação, porque não há servidor HTTP. Os papéis que middlewares preencheriam em uma aplicação web são cobertos por mecanismos de CLI/runtime Go:
| Papel típico de middleware | Equivalente no Quorum |
|---|---|
| Autenticação/autorização | N/A — sem contas/usuários (ferramenta local de CI). A única credencial, QUORUM_ADVICE_API_KEY, é uma chave de API de saída para o provider remoto opt-in, não auth de entrada |
| Logging de requisição | Logf injetado pelo controller (stderr, text/json via --log-format) |
| Timeout/cancelamento | context.Context propagado; context.WithTimeout por scanner e no probe |
| Tratamento central de erro | runCmd/toolVersion dobram o stderr; main.go centraliza o exit 2 |
| Recuperação de panic | Sem recover global; panics são erros de programação (ex.: registro duplicado) |
| Rate limiting | Parcial — backoff/retry no cliente OSV (osv.go) e no cliente HTTP do advisor |
| Proteção contra abuso/DoS | validateTargetRef (injeção de argumentos), checkTargetSize e capWriter (limites de tamanho); gate de consentimento de egress para o advisor remoto |
7. Workers (concorrência de fan-out)¶
Os "workers" do Quorum são goroutines efêmeras criadas pelo orchestrator para o fan-out paralelo de scanners. Não há pool de workers persistente nem fila de trabalho.
flowchart LR
R[orchestrator.Run] -->|wg.Add por adapter| G1[goroutine: trivy]
R --> G2[goroutine: grype]
R --> G3[goroutine: checkov]
R --> Gn[goroutine: ... até 12]
G1 -->|mu.Lock| AGG[(slice all + Runs)]
G2 --> AGG
G3 --> AGG
Gn --> AGG
AGG -->|wg.Wait| ENR[Enrich -> Merge]
Mecânica (em orchestrator.go):
- Uma goroutine por adapter selecionado; um
sync.WaitGroupsincroniza a conclusão. - Resultados são agregados sob um
sync.Mutex(res.Runse o sliceall). - Cada worker (
runOne) faz:Supports→ version probe com timeout (ProbeTime, padrão 60s) →Runcom timeout (PerScannerTime=--timeout) → classifica o status. - O
context.Contextflui do controller; cada worker deriva sub-contextos comWithTimeoute cancela no fim (cancelVer()/defer cancel). - Não há limite de paralelismo explícito (concorrência = número de adapters suportados, hoje ≤ 12).
Checklist de garantias de concorrência:
- [x] Acesso compartilhado protegido por mutex (
res.Runs,all). - [x] Cache de alias seguro para concorrência (
cache.StorecomRWMutex). - [x] Timeouts por scanner e por probe isolados via
context. - [x] Resultados ordenados deterministicamente após
wg.Wait(sort.SliceemRuns). - [x] Limite de stdout por scanner (
capWriter) para evitar OOM sob saída patológica. - [ ] Limite de paralelismo configurável (não existe — ver Premissas / proposta futura).
A camada de advisory roda depois do fan-out (single-threaded, no controller), então não adiciona concorrência a esta seção. O provider de IA é consultado sequencialmente por finding, limitado por
--advice-max.
8. Jobs / agendamento — N/A¶
N/A. Não há scheduler, cron interno, job em background nem tarefa recorrente dentro do processo. O Quorum é one-shot: um processo por invocação, terminando quando emite o relatório.
O agendamento, quando desejado, é externo ao backend: o GitHub Actions agenda/dispara a CLI
(via o composite action.yml ou a imagem :full/:slim). Aquecimentos como o cache do banco do grype
acontecem em build time da imagem Docker, não como job em runtime. Da mesma forma, quorum advise-index
(embutir o corpus OWASP) é um comando de pré-processamento pontual, não um job agendado.
Proposta futura (claramente separada): um modo
--watchou integração com schedulers externos poderia ser adicionado sem alterar o núcleo, mas não existe hoje.
9. Cache¶
Há duas camadas de cache, ambas apoiadas no mesmo store internal/cache (§4.2): o cache de
alias e o cache de advice de IA opt-in.
| Aspecto | Cache de alias | Cache de advice de IA (apenas --advice) |
|---|---|---|
| Backend | Arquivo JSON (map[string]string + schemaVersion) |
mesmo tipo de store |
| Chave / valor | id de vuln → id canônico (CVE preferido) | fingerprint+provider+model → recomendação |
| Local padrão | ~/.cache/quorum/aliases.json |
~/.cache/quorum/advice.json |
| Flag | --cache <path> |
--advice-cache <path> |
| Escrita | Atômica (.tmp + os.Rename), perm 0600, erros engolidos |
idem |
| Concorrência | sync.RWMutex |
idem |
| Invalidação | Sem TTL; schemaVersion invalida em mudança de formato |
chaveado por modelo, então mudar o modelo é uma nova chave |
| Tolerância a falha | Arquivo ausente/corrompido → cache vazio, scan prossegue | idem; também degrada para uma consulta ao vivo |
Não há cache de resultado de scan, nem cache HTTP do OSV além desse KV, nem cache distribuído em
memória. O pré-cache do banco do grype é um artefato da imagem :full (build time, com
GRYPE_DB_VALIDATE_AGE=false para não expirar), não uma camada de cache gerenciada pelo backend Go.
10. Queue / Messaging — N/A¶
N/A. Não há broker (Kafka/RabbitMQ/SQS), fila de mensagens interna nem comunicação inter-processos assíncrona. A justificativa técnica:
- O Quorum é um único processo stateless e efêmero; toda coordenação interna usa primitivas
in-process do Go (goroutines +
WaitGroup+Mutex+context). - O modelo de execução é fan-out/fan-in síncrono dentro de uma única invocação — não há produtor/consumidor desacoplado nem necessidade de durabilidade de mensagem.
- A "mensageria" entre estágios é simplesmente passar slices
[]model.Findingem memória pelo pipeline (orchestrator → correlate → consensus → filter → [advisory] → report).
Não há proposta de messaging: introduzi-la contradiria o princípio de ser uma CLI leve e sem daemon
("No panel, no daemon" — root.go).
11. Responsabilidade de cada pacote internal/*¶
Tabela de referência rápida (uma linha por pacote), fiel ao código lido:
| Pacote | Papel do template | Responsabilidade única | Arquivos | Depende de |
|---|---|---|---|---|
model |
Core | Tipos canônicos Finding/MergedFinding (incl. campos de advisory Remediation/References/Advice), Severity (com Rank), Resource, Location, Remediation/DocRef/Advice/Fix |
model.go |
— (folha) |
severity |
Utilitário | Normalização de severidade (de label, CVSS, Dockle), Max, AtLeast, Parse |
severity.go |
model |
purl |
Utilitário | Constrói/extrai identidade de pacote (name@version) para a chave VULN |
purl.go |
— |
adapter |
Repository/Gateway | Interface Adapter + registry + runCmd/toolVersion + helpers (capWriter/maxOutputBytes, extraArgs/splitArgs, redactSecretText); um arquivo por scanner com um parser |
adapter.go, trivy.go, grype.go, checkov.go, kics.go, dockle.go, kubescape.go, polaris.go, kubescore.go, terrascan.go, tfsec.go, regula.go, conftest.go |
model, severity, purl |
cache |
Repository (cache) | KV JSON persistente com flush atômico (0600), schemaVersion, thread-safe |
store.go |
— |
alias |
Serviço + gateway | Cadeia de resolução de id (aliases→cache→OSV); cliente HTTP OSV (id validado + PathEscape) |
resolver.go, osv.go |
cache |
crosswalk |
Serviço | Mapeia scanner\|ruleID → controle canônico (YAML derivado da saída real) |
crosswalk.go |
yaml.v3 |
correlate |
Serviço | Enriquecimento + CorrelationKey/Fingerprint determinísticos |
correlate.go, key.go |
alias, crosswalk, model, purl |
consensus |
Serviço | Agrupa por chave e computa Confidence/DetectionCount; ordena; scannerCategory (5 famílias) |
consensus.go |
model, severity |
filter |
Serviço | Baseline (.quorumignore) + corte --min-severity |
filter.go |
model, severity |
report |
Serviço | Serializa Result em SARIF/JSON/XML + métricas Prometheus (incl. séries de advisory) |
report.go, sarif.go, json.go, xml.go, metrics.go |
orchestrator |
orchestrator |
Aplicação | Fan-out paralelo, probe/timeout, status por scanner, orquestra o pipeline | orchestrator.go |
adapter, correlate, consensus, model |
enrich |
Advisory (opt-in) | Fase 0: carrega o knowledge pack e anexa templates de remediação curados + refs OWASP (determinístico, sem modelo) | enrich.go |
model, yaml.v3 |
rag |
Advisory (opt-in) | Fase 2: recuperação léxica/semântica do corpus OWASP com digest fixado; AttachReferences/GroundingText; embedder |
rag.go, lexical.go, embed.go |
model |
advisor |
Advisory (opt-in) | Fase 1/3: recomendação LLM local/remota + patch verify-the-fix; degradação graciosa; anexações rotuladas | advisor.go, client.go, prompt.go, verify.go |
model, cache, adapter (via Rescanner) |
evals |
Advisory (teste) | Harness offline que mede a cobertura de remediação determinística, a relevância de referências OWASP e a taxa de verify-the-fix (roda em CI, sem modelo pesado) | evals.go |
enrich, rag, model |
E os pacotes de comando:
| Pacote | Papel | Responsabilidade |
|---|---|---|
cmd/quorum |
Controller | main.go (entrada/saída), root.go (root cobra + list-scanners + advise-index), scan.go (comando scan + wiring + guardas de DoS + códigos de saída), advisor.go (wiring de advisory + rescanner verify-the-fix), advise_index.go (embute o corpus OWASP) |
12. Grafo de dependências (acíclico)¶
flowchart BT
model
severity --> model
purl
cache
crosswalk
alias --> cache
correlate --> alias
correlate --> crosswalk
correlate --> model
correlate --> purl
consensus --> model
consensus --> severity
filter --> model
filter --> severity
adapter --> model
adapter --> severity
adapter --> purl
orchestrator --> adapter
orchestrator --> correlate
orchestrator --> consensus
orchestrator --> model
report --> orchestrator
report --> model
enrich --> model
rag --> model
advisor --> model
advisor --> cache
evals --> enrich
evals --> rag
cmdquorum["cmd/quorum"] --> orchestrator
cmdquorum --> correlate
cmdquorum --> alias
cmdquorum --> cache
cmdquorum --> crosswalk
cmdquorum --> filter
cmdquorum --> report
cmdquorum --> severity
cmdquorum --> model
cmdquorum --> adapter
cmdquorum --> enrich
cmdquorum --> rag
cmdquorum --> advisor
Características do grafo: acíclico, model é folha e todo o domínio é testável sem
rede (o cliente OSV é injetado via a interface osvSource, stubável em testes; o Client e o
Rescanner do advisor também são interfaces). Os pacotes de advisory ficam no mesmo nível "quase-folha"
— dependem apenas de model (mais cache/adapter via interfaces), nunca o contrário.
13. Checklist de extensão (acionável)¶
Adicionar um novo scanner:
- [ ] Criar
internal/adapter/<scanner>.goimplementando a interfaceAdapter. - [ ] Chamar
Register(&<scanner>{})noinit(). - [ ] Implementar
Version,Supports,Capabilities,Run+ um parser paramodel.Finding. - [ ] Usar
runCmd(limite de saída) eextraArgs("<scanner>")para passthrough do operador. - [ ] Mapear a família de engine em
consensus.scannerCategory(sca/iac/k8s/hardening/policy). - [ ] Adicionar fixtures em
internal/adapter/testdatae o teste de contrato. - [ ] (Se IaC/k8s) adicionar regras YAML de crosswalk derivadas da saída real para os
ruleIDs do scanner (ou emitir umCanonicalControlnativo, como o tfsec faz com AVD).
Adicionar um novo formato de relatório:
- [ ] Adicionar um
Formatemreport/report.goe ocaseemWrite/ParseFormat. - [ ] Criar
report/<format>.gocom a funçãowrite<Format>(w, res). - [ ] Atualizar o help da flag
--formatemscan.go.
Estender a camada de advisory:
- [ ] Fase 0: adicionar entradas curadas ao knowledge pack (
knowledge/*.yaml), casadas porcanonicalControl/ruleId/category/type;enrich.Loadas coleta. - [ ] Fase 2: adicionar passagens a
knowledge/owasp/corpus.yaml; re-rodarquorum advise-indexpara atualizar o índice semântico (o content digest é preservado). Adicionar um caso ainternal/evals. - [ ] Manter toda anexação de IA rotulada "AI-generated, advisory only" e garantir que jamais mute chave/fingerprint/confidence/severidade/gate.
Premissas¶
- Versão de referência: o documento descreve o código da árvore atual (v0.8.3). A variável
versionemroot.goé"dev"como padrão de build (sobrescrita por-ldflags "-X main.version=..."no GoReleaser); a versão "real" do release vem da ldflag, não do código. - "Backend" = pacotes Go: interpretei "backend" como o conjunto
cmd/quorum+internal/*, já que não há servidor/serviço de longa duração. Itens do template orientados a web (middlewares HTTP, jobs/scheduler, queue/messaging) foram tratados como N/A com justificativa, conforme as regras de escrita. - Famílias de engine: a tabela
scannerCategoryemconsensus.gocobre os 12 adapters atualmente registrados, agrupados em 5 famílias (sca, iac, k8s, hardening, policy). - Limite de paralelismo: assumi que a concorrência do fan-out é igual ao número de adapters suportados (≤ 12 hoje), já que não há um limitador configurável no código.
- Detalhes de serialização SARIF/JSON/XML: li
report.go,sarif.go,metrics.goe o uso departialFingerprints["quorum/v1"]; o detalhe campo-a-campo do schema SARIF fica para Modelo de dados para evitar duplicação. - Pré-cache do grype: assumi que o banco do grype na imagem
:fullé populado em build time (Dockerfile.full, comGRYPE_DB_VALIDATE_AGE=false) e, portanto, não é uma camada de cache gerenciada pelo backend Go. - Diretório
./policydo conftest: o adapterconftestavalia o Rego do operador em./policy(ou viaQUORUM_CONFTEST_ARGS="--policy <dir>"). Sem políticas, o scanner é reportado comoerrorpor design (policy-as-code é opt-in); nenhum Rego versionado é distribuído no repo por padrão. - A camada de advisory é apenas apresentação: verifiquei em
scan.goque--adviceroda após o consensus e com as entradas de gating fixadas e anexa campos aoMergedFindingin place; ela nunca toca emCorrelationKey/Fingerprint/Confidence/severidade/--fail-on. O núcleo determinístico não tem IA; as Fases 1 (local) e 3 (remota) são estritamente opt-in, desligadas por padrão, com o provider remoto condicionado a consentimento explícito de egress, bloqueado por--offlinee recusando--fix.