Arquitetura¶
Este documento descreve a arquitetura do Quorum (v0.8.3), uma ferramenta CLI/Docker de consensus security scanning. O Quorum não é um scanner: ele orquestra um pool de 12 scanners OSS (trivy, grype, checkov, kics, dockle, kubescape, polaris, kube-score, terrascan, tfsec, regula, conftest), normaliza toda a saída para um modelo canônico (model.Finding), resolve aliases de vulnerabilidade, correlaciona findings equivalentes por uma chave determinística, calcula um score de confiança (consenso) e emite um relatório unificado (SARIF/JSON/XML). O estilo arquitetural escolhido é um Modular Monolith (binário Go único) organizado segundo Ports & Adapters (hexagonal) e estruturado como um Pipeline determinístico. Este documento justifica essas escolhas, mapeia as camadas para os pacotes reais do repositório e descreve o fluxo de execução com diagramas de componentes e de sequência.
Revisão: 2026-07-04 · versão do produto v0.8.3. Documentos relacionados: Visão geral · Modelo de dados / Design · CLI e flags · Supply chain e distribuição. Quando um link apontar para um arquivo ainda não escrito, trate-o como referência futura.
1. Sumário executivo¶
| Atributo | Valor |
|---|---|
| Estilo principal | Modular Monolith (um único binário Go) |
| Padrão de integração | Ports & Adapters (hexagonal) — interface adapter.Adapter |
| Padrão de processamento | Pipeline determinístico (scan → normalize → alias → correlate → score → report) |
| Concorrência | Fan-out paralelo (goroutines), um por scanner, com timeout por scanner |
| Estado | Sem estado persistente além de caches em disco (aliases, grype DB, advice de IA) |
| Linguagem / runtime | Go 1.26, CLI com cobra |
| Scanners integrados | 12 adapters (SCA, IaC/misconfig, policy-as-code, K8s posture, image hardening) |
| Comandos | scan <target>, list-scanners, advise-index |
| Distribuição | Imagens Docker :full (linux/amd64) e :slim (amd64+arm64) no GHCR + binários nativos via GoReleaser |
| Princípio de design | False split > false merge — na dúvida, não una findings |
| Camada de advice | Opt-in (--advice), apenas apresentação; o núcleo determinístico permanece sem IA |
2. Estilo arquitetural¶
2.1 Modular Monolith (binário único)¶
O Quorum compila para um único executável Go (cmd/quorum). Todos os módulos — orquestração, adapters, correlação, consenso, alias, crosswalk, filtro, relatório, métricas e a camada opcional de advice — vivem no mesmo processo e se comunicam por chamadas de função in-process, não por rede. A modularidade é garantida por fronteiras de pacote (internal/*) com responsabilidades únicas e dependências unidirecionais, não por separação em serviços.
Por que monolito modular:
- A unidade de trabalho é uma execução curta e batch. Um
quorum scanroda, produz um artefato (relatório) e termina. Não há tráfego contínuo, sessões, nem multitenancy que justifiquem processos de longa duração. - CI/CD é o ambiente alvo. O binário precisa ser fácil de baixar, pinar por digest e executar num runner ou container. Um único artefato assinado (cosign + SLSA) é trivial de auditar; um enxame de serviços não é.
- Latência e simplicidade. Passar
[]model.Findingentre etapas por chamada de função custa nanossegundos e zero serialização. A correlação precisa de todos os findings em memória ao mesmo tempo (agrupamento por chave) — distribuí-los só adicionaria custo. - Operação trivial. Sem orquestração de containers em runtime, sem service discovery, sem rede interna. O usuário roda um comando. Mesmo com 12 scanners, cada um permanece um subprocesso invocado in-process, não um serviço.
2.2 Ports & Adapters (hexagonal)¶
O coração da extensibilidade é a interface adapter.Adapter (internal/adapter/adapter.go), o port que isola o núcleo (orquestrador, correlação, consenso) das ferramentas externas. Cada scanner OSS é um adapter que sabe (a) invocar a CLI da ferramenta e (b) traduzir a saída nativa para model.Finding. Adicionar um scanner = adicionar um arquivo em internal/adapter/; nada no núcleo muda. Foi exatamente assim que o pool cresceu de 6 para 12 adapters entre a v0.2.3 e a v0.8.3.
// internal/adapter/adapter.go
type Adapter interface {
Name() string
Version(ctx context.Context) (string, error) // probe: detecta tool ausente/lenta
Supports(target Target) bool // este adapter cobre este alvo?
Capabilities() []Capability // tipos/alvos que produz
Run(ctx context.Context, target Target) ([]model.Finding, error)
}
Os 12 adapters registrados hoje, por família de engine (a mesma taxonomia usada pelo consenso — ver §6):
Família (scannerCategory) |
Adapters | Tipo de finding predominante |
|---|---|---|
sca |
trivy, grype | VULN |
iac |
checkov, kics, terrascan, tfsec, regula | MISCONFIG |
policy |
conftest | MISCONFIG (avalia SEU Rego de ./policy) |
k8s |
kubescape, polaris, kube-score | K8S_POSTURE |
hardening |
dockle | IMG_HARDENING |
Pontos hexagonais importantes, verificados no código:
- Registro por
init(). Cada adapter chamaadapter.Register(a)no seuinit(); o núcleo descobre adapters viaadapter.All()/adapter.Get(name)sem conhecer tipos concretos. Registro duplicado é panic (erro de programação). - O núcleo depende da abstração, não da implementação. O orquestrador opera sobre
[]adapter.Adapter. Trivy, grype, tfsec, conftest etc. são detalhes plugáveis. Supportsfiltra por alvo. trivy/grype cobremimage+repo; dockle sóimage; kube-score/polaris/kubescape cobremk8s(kubescape tambémrepo); os IaC (checkov/kics/terrascan/tfsec/regula) e conftest cobremrepo+k8s. O orquestrador nunca invoca um adapter num alvo que ele não suporta.- Passthrough por scanner. Cada adapter injeta argumentos extras do operador via
extraArgs(name), lidos deQUORUM_<NAME>_ARGS(ex.:QUORUM_CHECKOV_ARGS="--bc-api-key <key>"destrava políticas Prisma Cloud/Bridgecrew;QUORUM_CONFTEST_ARGS="--policy <dir>"aponta o Rego). O valor tem o mesmo nível de confiança das flags — é controlado por quem roda o container. - Adapters NÃO calculam identidade. Eles emitem
Findingcru/normalizado;CorrelationKeyeFingerprintsão responsabilidade centralizada deinternal/correlate— isso garante consistência entre ferramentas (DESIGN §5/§6). A exceção fiel ao código é o CanonicalControl já-canônico na origem: trivy emite ids AVD nativamente e o tfsec deriva o AVD do campolinks(avdInLink), de modo que ambos correlacionam sem depender do crosswalk. - Teste de contrato por adapter. Cada adapter tem fixtures versionadas em
internal/adapter/testdatae um teste (adapter_test.go,realdata_test.go) que quebra quando o formato de saída do scanner muda (antes da produção, não depois). - Endurecimento de I/O comum ao port.
runCmdfaz cap de saída emQUORUM_MAX_OUTPUT_BYTES(512 MiB por padrão) para evitar OOM por output-bomb, e um exit não-zero com stdout é tratado como sucesso (vários scanners saem não-zero justamente por acharem problemas). Findings de segredo têm oMatchredigido (redactSecretText) para não vazar o valor.
2.3 Pipeline determinístico¶
O processamento é um pipeline de estágios bem definidos, documentado no comentário de pacote do orquestrador e em DESIGN §3:
Cada estágio é uma transformação pura (ou quase-pura) sobre os dados do estágio anterior. Determinismo é um princípio explícito: a CorrelationKey é função pura dos dados normalizados (DESIGN princípio 4), o consenso ordena a saída de forma estável, e o Fingerprint é sha256(correlationKey). Mesma entrada ⇒ mesma saída ⇒ dedup temporal de graça (via partialFingerprints["quorum/v1"] no SARIF).
2.4 Camada de advice (opt-in, apenas apresentação)¶
Desde a v0.7.4 existe uma camada de advice que anexa orientação de remediação, referências OWASP e — quando explicitamente habilitado — recomendações em linguagem natural aos findings. Ela é opt-in via --advice e roda depois do consenso e do gating, como um estágio de apresentação pós-consenso. Ela nunca toca em correlationKey, fingerprint, confidence, severidade agregada ou o gate --fail-on. Sem --advice, a saída é byte-a-byte idêntica ao que era antes. O núcleo determinístico não tem IA; as partes de IA são estritamente opt-in e desligadas por padrão. Ela se materializa em três pacotes e quatro fases (todas implementadas):
- Fase 0 —
internal/enrich(determinístico, sem modelo). Templates de remediação curados + referências OWASP, casados porcanonicalControl/ruleId/category/type. Os dados vivem num knowledge pack versionado (knowledge/*.yaml: aws/azure/gcp/k8s/image/categories). Sem match, nada é anexado (a mesma regra "nunca chuta um match" do crosswalk). - Fase 2 —
internal/rag(RAG-as-artifact, determinístico). Recuperação a partir de um corpus OWASP versionado e PINADO POR DIGEST (knowledge/owasp/corpus.yaml). Recuperação lexical por padrão (sem modelo, totalmente air-gapped); semântica (embeddings) quando o corpus é embedado viaquorum advise-index. Oscanseleciona semântica automaticamente quando o corpus carrega vetores. Um corpus adulterado ou truncado falha o pin de digest e é recusado. - Fase 1 —
internal/advisor(LLM local opt-in).--advice-provider=localconsulta um endpoint OpenAI-compatível no próprio host (ex.: Ollama) para uma recomendação em linguagem natural, e--fix=suggestpropõe um patch que precisa passar por um re-scan de verify-the-fix (aplica numa cópia temporária, re-escaneia com o mesmo scanner, mantém apenas se o finding sumiu e o arquivo ainda parseia; nunca aplica automaticamente). Reprodutível viatemperature=0+ um cache em disco chaveado porfingerprint+provider+model. Degradação graciosa: se o modelo estiver inacessível o relatório sai sem advice de IA e o scan nunca falha. Toda anexação de IA é rotulada "AI-generated, advisory only". - Fase 3 —
internal/advisor(provedor remoto opt-in).--advice-provider=remotechama 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(isso faria upload de código-fonte). Apenas o finding normalizado é enviado — nunca o código-fonte.
Essas fases anexam os novos campos de MergedFinding Remediation, References e Advice (tipos model.Remediation/DocRef/Advice/Fix). Elas são cobertas por um harness de avaliação (internal/evals) que mede a cobertura determinística de remediação, a relevância das referências OWASP e a taxa de verify-the-fix no CI (sem modelo pesado). Ver Abordagem de IA e Proposta de IA.
3. Por que NÃO microservices / serverless / event-driven / CQRS¶
O template pede uma justificativa explícita de trade-offs. Cada estilo abaixo foi considerado e declarado N/A com fundamento técnico.
| Estilo | Veredito | Justificativa técnica |
|---|---|---|
| Microservices | N/A | A carga é batch, de curta duração e single-tenant. Quebrar correlação/consenso/relatório em serviços introduziria rede, serialização e service discovery sem nenhum ganho de escala ou isolamento — e quebraria o requisito central de distribuir um artefato assinável (cosign + SLSA). A correlação exige todos os findings dos 12 scanners em memória simultaneamente; distribuí-los seria contraproducente. |
| Serverless (FaaS) | N/A | Scanners pesados (checkov é um processo Python; grype precisa de DB de vulnerabilidades pré-cacheado de centenas de MB — no Quorum ele é embarcado na imagem com GRYPE_DB_VALIDATE_AGE=false para não expirar) violam limites de cold-start, tamanho de pacote e tempo de execução de funções. O ambiente alvo é o runner de CI, onde o binário já roda; FaaS adicionaria latência e custo. O timeout padrão de scan é 5m e o probe de versão tolera até 60s de cold-start — incompatível com FaaS típico. |
| Event-driven / mensageria | N/A | Não há produtores/consumidores assíncronos nem fluxo de eventos. O fan-out paralelo dos scanners já é feito in-process com goroutines + sync.WaitGroup; um broker (Kafka/NATS/SQS) seria infraestrutura sem propósito para um job que começa e termina. |
| CQRS | N/A | CQRS separa modelos de leitura e escrita sobre um datastore mutável. O Quorum não tem banco de dados relacional nem comandos que mutam estado compartilhado: as únicas "escritas" são o arquivo de relatório e o arquivo opcional de métricas (--metrics), e a única persistência é cache read-through (aliases, advice de IA). Sem domínio de escrita, não há nada a segregar. |
| Event Sourcing | N/A | Não há histórico de eventos de domínio a reconstruir; cada scan é independente e idempotente. |
Onde houver demanda futura legítima — por exemplo, um modo runtime/streaming (Falco/Tetragon) — o próprio DESIGN §2 já o classifica como produto separado com modelo de stream, fora do escopo deste binário batch. Ver "Propostas futuras" ao final.
4. Camadas e mapa de pacotes¶
A dependência flui em uma direção: a camada de CLI (controller) orquestra o pipeline; o pipeline depende do modelo canônico e das abstrações; os adapters dependem apenas do modelo. internal/model é o núcleo sem dependências.
| Camada | Pacote(s) | Responsabilidade |
|---|---|---|
| CLI / Controller | cmd/quorum (main.go, root.go, scan.go, advise_index.go, advisor.go) |
Parse de flags (cobra), validação de alvo (rejeita - → argument injection; cap QUORUM_MAX_TARGET_BYTES de 20 GiB), resolução de alvo/crosswalk/baseline, montagem das dependências, exit codes, sumário em stderr, métricas Prometheus (--metrics), formato de log (--log-format text\|json), montagem da camada de advice (--advice*, --fix) |
| Orquestração | internal/orchestrator |
Seleção de adapters por alvo, fan-out paralelo, probe de versão, timeout por scanner, status por scanner, coleta de findings |
| Adapters (port) | internal/adapter (12 arquivos: trivy.go, grype.go, checkov.go, kics.go, terrascan.go, tfsec.go, regula.go, conftest.go, kubescape.go, polaris.go, kubescore.go, dockle.go) |
Invocar CLI da ferramenta e traduzir para model.Finding; registro; probe Version; Supports/Capabilities; passthrough QUORUM_<NAME>_ARGS |
| Identidade / Correlação | internal/correlate (correlate.go, key.go) |
Enriquecer (alias + crosswalk), estampar CorrelationKey + Fingerprint |
| Resolução de alias | internal/alias (resolver.go, osv.go) |
CVE/GHSA → forma canônica (CVE preferido); cadeia local→cache→OSV (id validado + url.PathEscape) |
| Crosswalk | internal/crosswalk |
Carregar YAML rule→controle canônico (hub AVD para cloud, hub C-#### para k8s); schemaVersion versionado; resolver scanner\|ruleID |
| Consenso | internal/consensus |
Agrupar por CorrelationKey, agregar severidade, detectionCount, confidence (ponderando diversidade de família de engine), ordenação estável |
| Filtro / Gating | internal/filter |
Baseline (.quorumignore), --min-severity, supressões logadas |
| Camada de advice | internal/enrich, internal/rag, internal/advisor (+ internal/evals) |
Opt-in, apenas apresentação, pós-consenso: remediação curada + referências OWASP (Fase 0), recuperação OWASP pinada por digest (Fase 2, lexical/semântica), recomendações de IA local/remota opt-in + verify-the-fix (Fases 1/3). Anexa Remediation/References/Advice; nunca toca chave/fingerprint/confidence/gate |
| Relatório | internal/report (sarif.go, json.go, xml.go, metrics.go) |
Serializar Result/[]MergedFinding para SARIF (primário), JSON, XML; métricas em texto Prometheus (incluindo as métricas de advice, só sob --advice) |
| Suporte | internal/cache, internal/purl, internal/severity, internal/model |
Cache de aliases/advice (0600, schemaVersion); parsing/normalização de PURL; normalização de severidade; tipos canônicos (incl. Remediation/DocRef/Advice/Fix) |
Observações fiéis ao código:
- O controller (
scan.go) é quem monta as dependências: abre ocache.Store, cria oalias.OSVClient(a menos que--offline), carrega ocrosswalk(com fallback para/opt/quorum/crosswalkembarcado na imagem), constrói ocorrelate.Correlatore injeta tudo emorchestrator.Options. Isso mantém o orquestrador agnóstico de I/O de configuração. - Filtro e gating acontecem depois do consenso, no controller:
filter.Applyremove supressões/abaixo-do-mínimo deres.Mergedantes de emitir e de aplicar--fail-on. - A camada de advice roda depois do filtro, apenas sob
--advice:enrich.Load(...).Enrich, depoisrag.AttachReferences(erag.GroundingTextpara o prompt), depoisadvisor.New(...).Enrich. Ela apenas decora ores.Mergedsobrevivente e nunca reabre a decisão de gate. - Escrita segura: relatório com
filepath.Cleane perm0600(pode carregar detalhe sensível); métricas com0644(contagens não-sensíveis, feitas para scrape).
5. Diagrama de componentes (Mermaid)¶
flowchart TB
user([Usuário / CI runner]) -->|quorum scan target flags| CLI
subgraph controller["cmd/quorum — CLI / Controller (cobra)"]
CLI["scan.go<br/>valida alvo · resolve crosswalk/baseline<br/>monta dependências · exit codes<br/>--metrics · --log-format · --advice"]
end
CLI -->|orchestrator.Options| ORCH
subgraph core["Núcleo (in-process, binário único)"]
ORCH["internal/orchestrator<br/>seleciona adapters por alvo · fan-out paralelo<br/>probe de versão · timeout/scanner · status"]
subgraph ports["Adapters — Ports & Adapters (port: adapter.Adapter) — 12 scanners"]
SCA["sca:<br/>trivy · grype"]
IAC["iac:<br/>checkov · kics · terrascan<br/>tfsec · regula"]
POL["policy:<br/>conftest (seu Rego)"]
K8S["k8s:<br/>kubescape · polaris · kube-score"]
HARD["hardening:<br/>dockle"]
end
ORCH --> SCA & IAC & POL & K8S & HARD
CORR["internal/correlate<br/>enrich + CorrelationKey + Fingerprint"]
CONS["internal/consensus<br/>group · detectionCount · confidence<br/>(diversidade de família de engine)"]
FILT["internal/filter<br/>baseline + min-severity"]
ADV["Camada de advice (só --advice)<br/>internal/enrich · internal/rag · internal/advisor<br/>apenas apresentação · pós-consenso"]
REP["internal/report<br/>SARIF · JSON · XML · métricas"]
SCA & IAC & POL & K8S & HARD -->|"[]model.Finding"| ORCH
ORCH -->|"[]Finding"| CORR
CORR -->|"keyed []Finding"| CONS
CONS -->|"[]MergedFinding"| FILT
FILT -->|"kept"| ADV
ADV -.->|"--advice: Remediation/References/Advice"| REP
FILT -->|"sem --advice: byte-a-byte idêntico"| REP
end
subgraph deps["Dependências do correlate"]
ALIAS["internal/alias<br/>CVE/GHSA canônico"]
CW["internal/crosswalk<br/>rule → controle canônico<br/>hub AVD (cloud) · hub C-#### (k8s)"]
CACHE[("cache de aliases<br/>~/.cache/quorum/aliases.json (0600)")]
OSV{{"OSV.dev<br/>(desligado por --offline)"}}
end
CORR --> ALIAS
CORR --> CW
ALIAS --> CACHE
ALIAS -.->|fallback gracioso| OSV
subgraph advdeps["Dependências de advice (--advice)"]
KB[("knowledge/*.yaml<br/>remediação + refs OWASP")]
OWASP[("knowledge/owasp/corpus.yaml<br/>pinado por digest")]
LLM{{"LLM local/remoto<br/>(desligado por padrão)"}}
end
ADV --> KB
ADV --> OWASP
ADV -.->|"--advice-provider local\|remote"| LLM
REP -->|arquivo / stdout| OUT[["report.sarif|json|xml"]]
REP -.->|arquivo Prometheus| MET[["metrics.prom (--metrics)"]]
REP -.->|exit code 0/1/2| user
MODEL["internal/model<br/>(tipos canônicos, sem deps)"]
MODEL -.-> ports
MODEL -.-> CORR
MODEL -.-> CONS
MODEL -.-> ADV
Leitura do diagrama: o controller é a única camada com I/O de configuração; o orquestrador é o coordenador de concorrência; os 12 adapters (agrupados por família de engine) são plugáveis pela interface adapter.Adapter; correlação/consenso/filtro/relatório são estágios sequenciais do pipeline; a camada de advice fica depois do filtro e só decora o relatório sob --advice; internal/model é o núcleo do qual todos dependem mas que não depende de ninguém.
6. Diagrama de sequência do pipeline (Mermaid)¶
Fluxo scan → normalize → alias → correlate → score → report para um scan típico.
sequenceDiagram
autonumber
actor U as Usuário/CI
participant C as cmd/quorum (scan.go)
participant O as orchestrator
participant A as adapters (até 12 em paralelo)
participant R as correlate
participant AL as alias
participant X as crosswalk
participant K as consensus
participant F as filter
participant ADV as advice (--advice)
participant P as report
U->>C: quorum scan target --type ... --format sarif
C->>C: valida alvo (rejeita '-', cap de tamanho)
C->>C: resolve crosswalk dir (fallback /opt/quorum/crosswalk), baseline
C->>C: monta cache + OSV (se !offline) + Correlator
C->>O: Run(ctx, target, Options{Scanners, PerScannerTime, Correlator})
O->>O: selectAdapters(target, scanners) — filtra por Supports
note over O,A: fan-out: 1 goroutine por adapter, WaitGroup
par scan (paralelo)
O->>A: Supports(target)? Version(ctx) [probe 60s]
note right of A: distingue timeout / killed(OOM) / não-instalado
A-->>O: status = ran|skipped|unavailable|error|timeout
O->>A: Run(ctx, target) [timeout por scanner + cap de output]
A->>A: normalize: saída nativa → []model.Finding
A-->>O: []model.Finding (canônico)
end
O->>O: junta todos os findings + ScannerRun[] (status)
O->>R: Enrich(ctx, allFindings)
loop por finding
alt Type == VULN
R->>AL: Canonical(id, knownAliases)
AL->>AL: 1) aliases locais → 2) cache → 3) OSV (CVE preferido)
AL-->>R: id canônico (degrada gracioso se rede falha)
else MISCONFIG / K8S_POSTURE / IMG_HARDENING
R->>R: CanonicalControl já preenchido? (trivy AVD, tfsec AVD via links) → mantém
R->>X: senão Resolve(scanner, ruleID)
X-->>R: controle canônico (AVD/C-####) ou Unmapped=true: nunca chuta match
end
R->>R: CorrelationKey = BuildKey(f); Fingerprint = sha256(key)
end
R-->>O: []Finding com chave/fingerprint
O->>K: Merge(findings)
K->>K: agrupa por CorrelationKey
K->>K: detectionCount = scanners distintos, severidade agregada (max)
K->>K: confidence = f(count, diversidade de família, severidade, autoritativo)
K->>K: ordena estável (severidade, confidence, count, key)
K-->>O: []MergedFinding
O-->>C: Result{Runs, Findings, Merged, Duration}
C->>F: Apply(merged, minSeverity, baseline)
F->>F: suprime por fingerprint/correlationKey + abaixo de min-severity
F-->>C: kept (supressões sempre logadas)
opt --advice (apenas apresentação, após as entradas de gating fixadas)
C->>ADV: enrich.Enrich → rag.AttachReferences → advisor.Enrich
ADV->>ADV: Fase 0 templates + refs OWASP (determinístico)
ADV->>ADV: Fase 2 recupera do corpus pinado por digest (lexical/semântica)
ADV->>ADV: Fase 1/3 recomendação local/remota (rotulada, graciosa) + verify-the-fix
ADV-->>C: Remediation/References/Advice anexados (chave/fingerprint/gate intactos)
end
C->>P: Write(buf, result, format) [+ WriteMetrics se --metrics]
P-->>C: SARIF/JSON/XML
C->>U: escreve arquivo / stdout + sumário (stderr)
C->>U: exit 0 (ok) | 1 (--fail-on disparou) | 2 (erro)
Pontos fiéis ao código que o diagrama reflete:
- O probe de versão roda com timeout próprio (
Options.ProbeTime, default 60s) e classifica a falha: timeout (lento/sem memória), killed (provável OOM, viasignal: killed) ou não-instalado. O status nunca confunde "0 findings" com "não rodou" (DESIGN §14, "0 findings is not proof of safety"). - Um exit não-zero de scanner com saída em stdout é tratado como sucesso (
runCmd): vários scanners saem não-zero justamente por terem encontrado problemas. - A resolução de controle no
correlaterespeitaCanonicalControljá preenchido na origem (trivy emite AVD nativo; tfsec deriva AVD dolinks), consultando o crosswalk apenas quando o adapter não trouxe controle canônico. Isso faz tfsec e trivy correlacionarem sem entrada de crosswalk. - O crosswalk resolve por dois hubs, DERIVADOS de output real (regra false split > false merge): AVD para nuvem (
aws.yaml/azure.yaml/gcp.yaml: S3/IAM/EBS/SG/RDS/KMS/CloudTrail/VPC-flow-logs, Azure Storage/Key Vault, GCP bucket/firewall/SQL) e C-#### (controles kubescape) para k8s (k8s.yaml: privilege-escalation, privileged, non-root, limites de cpu/mem, probes, read-only-fs, linux-hardening, automount-SA, network-policy, host-network, host-PID/IPC, capabilities, secrets — cruzando kubescape × polaris × kube-score). - RBAC segue single-engine: a análise de RBAC do kubescape exige contexto de cluster ao vivo, sem contraparte cross-engine equivalente, então não entra no crosswalk k8s (documentado; nunca se força um match).
- Se o
Correlatorfornil, o orquestrador ainda estampaCorrelationKey/Fingerprint(viaBuildKey/Fingerprint) para permitir agrupamento — apenas pula o enriquecimento (alias/crosswalk). - A etapa de advice (apenas
--advice) roda depois do filtro/gate, sobre ores.Mergedsobrevivente; ela degrada graciosamente (um modelo inacessível não gera advice e nunca falha o scan) e nunca muta chave/fingerprint/confidence/severidade ou o gate.
7. Decisões e trade-offs registrados¶
| Decisão | Alternativa rejeitada | Trade-off aceito |
|---|---|---|
| Binário único (monolito modular) | Microservices/FaaS | Menos isolamento de falha entre estágios; ganha simplicidade, assinabilidade e latência |
| Ports & Adapters via interface | Acoplar scanners no núcleo | Um pouco mais de boilerplate por adapter; ganha extensibilidade — foi assim que se foi de 6 para 12 scanners sem tocar no core |
| Fan-out com goroutines + timeout/scanner | Execução sequencial | Maior pico de memória (até 12 scanners ao mesmo tempo); ganha tempo de parede |
| Probe de 60s generoso | Probe curto | Scan demora mais a marcar tool ausente; evita falso "unavailable" em cold-start/runner com pouca RAM |
| False split > false merge | Merge agressivo | Mais findings duplicados aparentes; nunca esconde risco por merge errado. O próprio crosswalk é derivado de output real sob essa regra |
| Consenso por diversidade de família de engine | Contagem crua de detecções | Duas engines da mesma família contam menos que duas famílias distintas; mais nuance no confidence, mais complexidade no score |
| Crosswalk multi-hub (AVD para cloud, C-#### para k8s) | Um único hub universal | Precisa manter mapeamentos por domínio; ganha correlação real entre engines de IaC e de posture |
CanonicalControl na origem (trivy/tfsec AVD) |
Sempre passar pelo crosswalk | Duas fontes de verdade para o controle; ganha correlação tfsec↔trivy sem entrada de mapeamento |
| Cache de alias read-through | Sempre consultar OSV | Possível staleness do cache; ganha idempotência e velocidade em CI, e funciona offline |
Crosswalk com Unmapped flag |
Inferir match | Findings isolados quando não mapeados; nunca inventa correlação |
| Policy-as-code trazida pelo usuário (conftest/Rego) | Regras embutidas de política | Sem política padrão (conftest sem Rego reporta error, esperado); ganha flexibilidade total ao operador |
| Camada de advice opt-in, apenas apresentação | Embutir IA no núcleo, ou não dar orientação alguma | Uma superfície opt-in a mais (flags, knowledge pack, modelo opcional); o núcleo determinístico permanece sem IA e byte-a-byte idêntico sem --advice, enquanto operadores que quiserem recebem orientação de remediação/OWASP/IA |
8. Atributos de qualidade (mapeamento)¶
- Extensibilidade: novo scanner = novo arquivo em
internal/adapter+ fixture de contrato; zero mudança no núcleo (comprovado: 6→12 adapters). - Determinismo/Idempotência: chaves e fingerprints são funções puras; consenso ordena de forma estável; mesma entrada ⇒ mesmo SARIF. A camada de advice é determinística nas Fases 0/2 e reprodutível na Fase 1 (
temperature=0+ cache chaveado por fingerprint). - Resiliência: degradação graciosa em falha de rede (alias/OSV e o advisor de IA); status explícito por scanner; timeout isolado por scanner não derruba os demais; caps de DoS (
QUORUM_MAX_OUTPUT_BYTES512 MiB,QUORUM_MAX_TARGET_BYTES20 GiB) contra output/target bombs. - Observabilidade: logs de progresso em stderr (
--log-format text|json, silenciáveis com--quiet), sumário por scanner com status, supressões sempre logadas, métricas Prometheus opcionais (--metrics, textfile collector). Sob--adviceas métricas acrescentamquorum_advice_enriched{kind=remediation|references|recommendation},quorum_advice_provider{provider}equorum_advice_fix{stage=proposed|verified}(a taxa de verify-the-fix). - Segurança da cadeia: artefato único assinado keyless (cosign/OIDC, com retry) + atestação SLSA build-provenance e SBOM SPDX atestada (por imagem e por binário); o knowledge pack + crosswalk também recebem uma atestação SLSA build-provenance a cada release (verifique com
gh attestation verify knowledge/owasp/corpus.yaml); bases pinadas porsha256; binários de scanner de terceiros (kubescape/tfsec/terrascan/regula/conftest) verificados por checksum; imagens:full/:slimno GHCR; ver 10-infraestrutura.md. - Segurança de entrada: alvo iniciando com
-recusado (argument injection);--outputcomfilepath.Cleane perm0600; id de OSV validado eurl.PathEscape; cache0600comschemaVersion; segredos redigidos noMatch. Para a camada de advice, egress remoto é condicionado a consentimento explícito, bloqueado por--offline, e--fixrecusa remoto (sem upload de código-fonte). - Testabilidade: contract tests por adapter; injeção de dependências no controller permite stub de OSV/cache nos testes; um harness de evals (
internal/evals) mede a qualidade do advice no CI; cobertura de testes reportada no CI.
9. Checklist de conformidade arquitetural¶
Use ao adicionar/alterar componentes para manter a arquitetura íntegra.
- [ ] Novo scanner implementa toda a interface
adapter.Adapter(Name/Version/Supports/Capabilities/Run). - [ ] Novo adapter chama
adapter.Registernoinit()e não colide com nome existente. - [ ]
Supportsrestringe corretamente os alvos (image|repo|k8s); o orquestrador nunca roda o adapter num alvo não suportado. - [ ] Adapter não calcula
CorrelationKey/Fingerprint(responsabilidade deinternal/correlate). Exceção legítima: preencherCanonicalControlquando a ferramenta já emite AVD (trivy/tfsec). - [ ] Adapter possui fixture versionada em
internal/adapter/testdatae contract test. - [ ]
Runtraduz a saída paramodel.Finding; nenhuma lógica de negócio opera sobre JSON cru de scanner; usarunCmd/extraArgs(respeitando cap de output e passthroughQUORUM_<NAME>_ARGS). - [ ] Falhas de rede (OSV) degradam graciosamente, nunca falham o scan inteiro.
- [ ] Mudanças no pipeline preservam determinismo (chave = função pura dos dados normalizados).
- [ ] Status de scanner reportado corretamente (
ran|skipped|unavailable|error|timeout); "0 findings" nunca mascara "não rodou". - [ ] Nova entrada de crosswalk mapeia para o hub correto (AVD para cloud, C-#### para k8s) e é derivada de output real; sem match, marca
Unmappedem vez de chutar. - [ ] A camada de advice permanece opt-in e apenas apresentação: nunca deve tocar
correlationKey/fingerprint/confidence/severidade agregada ou o gate--fail-on, e sem--advicea saída permanece byte-a-byte idêntica. Provedores remotos permanecem condicionados a--advice-allow-egress, bloqueados por--offlinee recusam--fix. - [ ] Nenhuma dependência nova reintroduz frontend web, banco relacional ou API REST, nem um runtime de longa duração; qualquer IA permanece na camada de advice opt-in (nunca no núcleo determinístico).
10. Não-objetivos e propostas futuras (claramente separadas)¶
Não-objetivos (N/A por design): frontend web, banco de dados relacional, API REST HTTP, autenticação/contas de usuário, e runtime/cloud de longa duração. Justificativa: o Quorum é um job batch CLI/Docker single-tenant cujo contrato é "entra alvo, sai relatório + exit code". Esses componentes exigiriam um modelo operacional incompatível com o artefato único assinável e com a execução em runner de CI. Essa fronteira segue inalterada da v0.2.3 à v0.8.3 — o produto cresceu em profundidade (mais scanners, consenso multi-cloud/k8s, supply chain endurecida e uma camada de advice opt-in), não em superfície arquitetural.
Sobre IA: IA não é mais um não-objetivo, mas é deliberadamente mantida fora do núcleo determinístico. A camada de advice (§2.4) é opt-in via
--advice, apenas apresentação e desligada por padrão — sem ela a saída é byte-a-byte idêntica e não contém IA. O núcleo de scanning/correlação/consenso/gating permanece totalmente determinístico e sem modelo.
Já implementado desde a v0.2.3 (era roadmap, hoje é comportamento atual):
- Consenso além de SCA: correlação de MISCONFIG/IaC e K8s posture via crosswalk multi-hub (AVD para nuvem, C-#### para k8s), cruzando checkov/kics/terrascan/tfsec/regula e kubescape/polaris/kube-score.
- Policy-as-code opcional (Conftest/OPA): o usuário traz seu Rego (
./policy), integrado ao mesmo relatório e consenso — antes previsto para a v1.0, já entregue. - Distribuição endurecida: GitHub Action composite que cosign-verifica a imagem e auto-monta
/var/run/docker.sockem alvosimage; tag móvelv0avançada automaticamente a cada release semver; atestações SLSA/SBOM. - Camada de advice (desde a v0.7.4):
--adviceopt-in com templates de remediação determinísticos + referências OWASP (Fase 0), recuperação OWASP pinada por digest (Fase 2) e uma recomendação de IA local/remota opt-in + verify-the-fix (Fases 1/3), além do subcomandoquorum advise-index, das métricas de advice e de um harness de evals. A GitHub Action (action.yml) agora expõe todos os inputs de advice e o knowledge pack recebe sua própria atestação SLSA.
Propostas futuras (não implementadas hoje):
- Módulo runtime separado (Falco ou Tetragon): modelo de stream, fora deste binário batch — seria um produto à parte (DESIGN §2/§13).
- Perfis de imagem (
:sca,:iac,:k8s) caso o tamanho da:fullincomode (DESIGN §12).
Estas propostas são roadmap, não comportamento atual da v0.8.3.
Premissas¶
- Tomei o código da branch
main(v0.8.3) como fonte de verdade. Onde DESIGN.md diverge do código, segui o código — por exemplo, a assinatura real dealias.Resolver.Canonical(ctx, id, knownAliases), odefaultProbeTime = 60semorchestrator.go, a taxonomia de famílias de engine emconsensus.scannerCategory, o fato de trivy/tfsec já emitiremCanonicalControlAVD na origem e o fato de a camada de advice (internal/enrich/rag/advisor) rodar depois do filtro e apenas sob--advice. - Contei 12 adapters registrados em
internal/adapter(trivy, grype, checkov, kics, terrascan, tfsec, regula, conftest, kubescape, polaris, kube-score, dockle), confirmados pelos respectivosName()/Register(). Assumi queinternal/adapter/testdatacontém as fixtures de contrato citadas no DESIGN §5/§14; a presença dos testes (adapter_test.go,realdata_test.go) confirma o padrão sem inspecionar cada fixture. - Os detalhes de distribuição/supply chain (imagens
:fulllinux/amd64 e:slimamd64+arm64, cosign keyless com retry, SLSA build-provenance, SBOM SPDX atestada, atestação SLSA do knowledge pack, GitHub Action composite, tag móvelv0viatag-major.yml) vêm do briefing do produto e dos manifestos de release; este documento os referencia mas não os auditou linha a linha emrelease.yml/action.yml/.goreleaser.yaml— ver 10-infraestrutura.md para a fonte autoritativa. - O conteúdo dos hubs de crosswalk (AVD para
aws/azure/gcp.yaml, C-#### parak8s.yaml, cobertura de controles) foi tomado do briefing e da presença dos arquivos emcrosswalk/; o mecanismo de carga (crosswalk.Load,schemaVersion, fallback/opt/quorum/crosswalk) foi verificado no código. - O diagrama de sequência representa o caminho feliz com
Correlatornão-nulo e--offlinedesligado; variações (offline, correlator nil, scanner indisponível, conftest sem Rego →errore a camada de advice degradando quando o modelo está inacessível) estão descritas em texto.