Ir para o conteúdo

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." O Raw map[string]any é mantido mas marcado json:"-" (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:

  1. 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 no Correlator. Também valida as flags de advisory (--advice-provider, --fix, consentimento de egress) antes de qualquer trabalho.
  2. 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.
  3. Correlate enriquece (resolve aliases para VULN, controles para MISCONFIG/K8S_POSTURE/ IMG_HARDENING) e carimba CorrelationKey + Fingerprint.
  4. Consensus agrupa por CorrelationKey e calcula Confidence/DetectionCount.
  5. Filter aplica --min-severity e a baseline .quorumignore.
  6. 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.
  7. Report serializa o relatório e, se --metrics foi passada, escreve as métricas Prometheus (incluindo as séries de advisory quando --advice está 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).

  • Options carrega: Scanners []string, PerScannerTime (timeout por scanner), ProbeTime (timeout do version-probe, padrão 60s via defaultProbeTime), Correlator e Logf.
  • Result agrega Runs []ScannerRun, Findings []model.Finding (bruto, para detalhe JSON), Merged []model.MergedFinding, Duration.
  • ScannerRun.Status assume ran | skipped | unavailable | error | timeout — a transparência de status é deliberada: "0 vulns nunca pode parecer que o scan não rodou".
  • runOne distingue as causas de falha do probe: timeout (DeadlineExceeded), morto por sinal/OOM (killedSignal detecta "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 computa CorrelationKey/Fingerprint via correlate.BuildKey/Fingerprint para 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 ser nil (degrada para id-como-está / não-mapeado).
  • Enrich itera sobre os findings: TypeVulnresolveVuln (alias); TypeMisconfig/ TypeK8sPosture/TypeImgHardeningresolveControl (crosswalk). Depois aplica BuildKey + 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 MISCONFIG usa basename + tipo de recurso + controle (engines discordam sobre caminho e identidade de recurso); a chave K8S_POSTURE usa 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 em key.go).
  • Fingerprint(key) = sha256(key) em hex — é o partialFingerprints["quorum/v1"] no SARIF.
  • controlKey prefere o controle canônico resolvido; quando não-mapeado, usa UNMAPPED:<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, diversidade 0.25, severidade 0.25, autoritativo 0.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 Confirmed ou 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).

  • chainResolver resolve 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.
  • --offline passa osv = nil, desabilitando a camada 3 (usa apenas aliases locais + cache).
  • OSVClient tem 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).
  • Resolve retorna ok=false quando não há mapeamento — o chamador mantém o finding isolado e o marca Unmapped ("nunca adivinhe um match", DESIGN §6).
  • O diretório padrão é ./crosswalk com fallback automático para /opt/quorum/crosswalk (o bundle da imagem Docker), via resolveCrosswalkDir em scan.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 em CanonicalControl). Ver tfsec.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.

  • Baseline casa por Fingerprint OU CorrelationKey (o usuário pode copiar qualquer um do relatório); o arquivo .quorumignore (padrão), comentários #, linhas em branco ignoradas.
  • LoadBaseline distingue "ausente" (ok=false) de "presente porém vazio" — o controller exige existência apenas quando --baseline foi passado explicitamente.
  • Apply retorna Result{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.
  • ParseFormat valida --format.
  • SARIF carrega partialFingerprints["quorum/v1"] derivado de Fingerprint.
  • 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} e quorum_multi_detected. Quando --advice está 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 via temperature=0 mais 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 interface Rescanner, apoiada no registry de adapters em cmd/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 via QUORUM_ADVICE_API_KEY). Dados saem do host, então é condicionado a consentimento explícito (--advice-allow-egress), bloqueado por --offline e 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 seu init(); Get/All/Names expõem o registry. Um nome duplicado causa panic (erro de programação).
  • Execução de processo: runCmd roda 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). toolVersion faz 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, usando severity.FromLabel/FromCVSS/FromDockle e purl.Build. Adapters nunca computam CorrelationKey — isso é centralizado no correlator.
  • Testes de contrato: cada adapter tem um teste de contrato contra fixtures em internal/adapter/testdata (ver adapter_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:

  • conftest não tem regras embutidas: avalia seu próprio Rego (em ./policy por padrão, ou via QUORUM_CONFTEST_ARGS="--policy <dir>"). Sem políticas ele dá erro e o scanner é reportado como error — esperado, já que policy-as-code é opt-in.
  • tfsec deriva o AVD do link e armazena em CanonicalControl, correlacionando com trivy sem crosswalk.
  • kubescape escreve 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).
  • Put escreve com rename atômico (.tmp → destino, perm 0600) 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 um schemaVersion para 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 (via os.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ão 5m), --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 ./-name para um caminho literal).
  • checkTargetSize: para alvos repo/k8s, faz um walk que para assim que o limite (QUORUM_MAX_TARGET_BYTES, padrão 20 GiB) é ultrapassado; alvos image são pulados.
  • resolveTargetType: se --type for 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 --advice produz exatamente o mesmo resultado de gate que sem ela.

5.4 Saída de progresso, resumo e artefatos

  • O controller injeta um Logf que escreve em stderr (silenciável via --quiet) em dois formatos: text (prefixo [quorum]) ou json (linha {ts,level,msg}), conforme --log-format. Sob --advice ele 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 com filepath.Clean e escrito com perm 0600 (pode carregar detalhe sensível de finding). O arquivo --metrics é escrito com 0644 (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.WaitGroup sincroniza a conclusão.
  • Resultados são agregados sob um sync.Mutex (res.Runs e o slice all).
  • Cada worker (runOne) faz: Supports → version probe com timeout (ProbeTime, padrão 60s) → Run com timeout (PerScannerTime = --timeout) → classifica o status.
  • O context.Context flui do controller; cada worker deriva sub-contextos com WithTimeout e 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.Store com RWMutex).
  • [x] Timeouts por scanner e por probe isolados via context.
  • [x] Resultados ordenados deterministicamente após wg.Wait (sort.Slice em Runs).
  • [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 --watch ou 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.Finding em 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>.go implementando a interface Adapter.
  • [ ] Chamar Register(&<scanner>{}) no init().
  • [ ] Implementar Version, Supports, Capabilities, Run + um parser para model.Finding.
  • [ ] Usar runCmd (limite de saída) e extraArgs("<scanner>") para passthrough do operador.
  • [ ] Mapear a família de engine em consensus.scannerCategory (sca/iac/k8s/hardening/policy).
  • [ ] Adicionar fixtures em internal/adapter/testdata e o teste de contrato.
  • [ ] (Se IaC/k8s) adicionar regras YAML de crosswalk derivadas da saída real para os ruleIDs do scanner (ou emitir um CanonicalControl nativo, como o tfsec faz com AVD).

Adicionar um novo formato de relatório:

  • [ ] Adicionar um Format em report/report.go e o case em Write/ParseFormat.
  • [ ] Criar report/<format>.go com a função write<Format>(w, res).
  • [ ] Atualizar o help da flag --format em scan.go.

Estender a camada de advisory:

  • [ ] Fase 0: adicionar entradas curadas ao knowledge pack (knowledge/*.yaml), casadas por canonicalControl/ruleId/category/type; enrich.Load as coleta.
  • [ ] Fase 2: adicionar passagens a knowledge/owasp/corpus.yaml; re-rodar quorum advise-index para atualizar o índice semântico (o content digest é preservado). Adicionar um caso a internal/evals.
  • [ ] Manter toda anexação de IA rotulada "AI-generated, advisory only" e garantir que jamais mute chave/fingerprint/confidence/severidade/gate.

Premissas

  1. Versão de referência: o documento descreve o código da árvore atual (v0.8.3). A variável version em root.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.
  2. "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.
  3. Famílias de engine: a tabela scannerCategory em consensus.go cobre os 12 adapters atualmente registrados, agrupados em 5 famílias (sca, iac, k8s, hardening, policy).
  4. 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.
  5. Detalhes de serialização SARIF/JSON/XML: li report.go, sarif.go, metrics.go e o uso de partialFingerprints["quorum/v1"]; o detalhe campo-a-campo do schema SARIF fica para Modelo de dados para evitar duplicação.
  6. Pré-cache do grype: assumi que o banco do grype na imagem :full é populado em build time (Dockerfile.full, com GRYPE_DB_VALIDATE_AGE=false) e, portanto, não é uma camada de cache gerenciada pelo backend Go.
  7. Diretório ./policy do conftest: o adapter conftest avalia o Rego do operador em ./policy (ou via QUORUM_CONFTEST_ARGS="--policy <dir>"). Sem políticas, o scanner é reportado como error por design (policy-as-code é opt-in); nenhum Rego versionado é distribuído no repo por padrão.
  8. A camada de advisory é apenas apresentação: verifiquei em scan.go que --advice roda após o consensus e com as entradas de gating fixadas e anexa campos ao MergedFinding in place; ela nunca toca em CorrelationKey/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 --offline e recusando --fix.