Ir para o conteúdo

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 scan roda, 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.Finding entre 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 chama adapter.Register(a) no seu init(); o núcleo descobre adapters via adapter.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.
  • Supports filtra por alvo. trivy/grype cobrem image+repo; dockle só image; kube-score/polaris/kubescape cobrem k8s (kubescape também repo); os IaC (checkov/kics/terrascan/tfsec/regula) e conftest cobrem repo+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 de QUORUM_<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 Finding cru/normalizado; CorrelationKey e Fingerprint são responsabilidade centralizada de internal/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 campo links (avdInLink), de modo que ambos correlacionam sem depender do crosswalk.
  • Teste de contrato por adapter. Cada adapter tem fixtures versionadas em internal/adapter/testdata e 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. runCmd faz cap de saída em QUORUM_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 o Match redigido (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:

scan → normalize → resolve aliases → correlate → score → report

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 por canonicalControl / 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 via quorum advise-index. O scan seleciona 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=local consulta um endpoint OpenAI-compatível no próprio host (ex.: Ollama) para uma recomendação em linguagem natural, e --fix=suggest propõ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 via temperature=0 + um cache em disco chaveado por fingerprint+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=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 (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 o cache.Store, cria o alias.OSVClient (a menos que --offline), carrega o crosswalk (com fallback para /opt/quorum/crosswalk embarcado na imagem), constrói o correlate.Correlator e injeta tudo em orchestrator.Options. Isso mantém o orquestrador agnóstico de I/O de configuração.
  • Filtro e gating acontecem depois do consenso, no controller: filter.Apply remove supressões/abaixo-do-mínimo de res.Merged antes de emitir e de aplicar --fail-on.
  • A camada de advice roda depois do filtro, apenas sob --advice: enrich.Load(...).Enrich, depois rag.AttachReferences (e rag.GroundingText para o prompt), depois advisor.New(...).Enrich. Ela apenas decora o res.Merged sobrevivente e nunca reabre a decisão de gate.
  • Escrita segura: relatório com filepath.Clean e perm 0600 (pode carregar detalhe sensível); métricas com 0644 (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, via signal: 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 correlate respeita CanonicalControl já preenchido na origem (trivy emite AVD nativo; tfsec deriva AVD do links), 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 Correlator for nil, o orquestrador ainda estampa CorrelationKey/Fingerprint (via BuildKey/Fingerprint) para permitir agrupamento — apenas pula o enriquecimento (alias/crosswalk).
  • A etapa de advice (apenas --advice) roda depois do filtro/gate, sobre o res.Merged sobrevivente; 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_BYTES 512 MiB, QUORUM_MAX_TARGET_BYTES 20 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 --advice as métricas acrescentam quorum_advice_enriched{kind=remediation|references|recommendation}, quorum_advice_provider{provider} e quorum_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 por sha256; binários de scanner de terceiros (kubescape/tfsec/terrascan/regula/conftest) verificados por checksum; imagens :full/:slim no GHCR; ver 10-infraestrutura.md.
  • Segurança de entrada: alvo iniciando com - recusado (argument injection); --output com filepath.Clean e perm 0600; id de OSV validado e url.PathEscape; cache 0600 com schemaVersion; segredos redigidos no Match. Para a camada de advice, egress remoto é condicionado a consentimento explícito, bloqueado por --offline, e --fix recusa 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.Register no init() e não colide com nome existente.
  • [ ] Supports restringe corretamente os alvos (image|repo|k8s); o orquestrador nunca roda o adapter num alvo não suportado.
  • [ ] Adapter não calcula CorrelationKey/Fingerprint (responsabilidade de internal/correlate). Exceção legítima: preencher CanonicalControl quando a ferramenta já emite AVD (trivy/tfsec).
  • [ ] Adapter possui fixture versionada em internal/adapter/testdata e contract test.
  • [ ] Run traduz a saída para model.Finding; nenhuma lógica de negócio opera sobre JSON cru de scanner; usa runCmd/extraArgs (respeitando cap de output e passthrough QUORUM_<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 Unmapped em 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 --advice a saída permanece byte-a-byte idêntica. Provedores remotos permanecem condicionados a --advice-allow-egress, bloqueados por --offline e 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.sock em alvos image; tag móvel v0 avançada automaticamente a cada release semver; atestações SLSA/SBOM.
  • Camada de advice (desde a v0.7.4): --advice opt-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 subcomando quorum 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 :full incomode (DESIGN §12).

Estas propostas são roadmap, não comportamento atual da v0.8.3.


Premissas

  1. 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 de alias.Resolver.Canonical(ctx, id, knownAliases), o defaultProbeTime = 60s em orchestrator.go, a taxonomia de famílias de engine em consensus.scannerCategory, o fato de trivy/tfsec já emitirem CanonicalControl AVD na origem e o fato de a camada de advice (internal/enrich/rag/advisor) rodar depois do filtro e apenas sob --advice.
  2. Contei 12 adapters registrados em internal/adapter (trivy, grype, checkov, kics, terrascan, tfsec, regula, conftest, kubescape, polaris, kube-score, dockle), confirmados pelos respectivos Name()/Register(). Assumi que internal/adapter/testdata conté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.
  3. Os detalhes de distribuição/supply chain (imagens :full linux/amd64 e :slim amd64+arm64, cosign keyless com retry, SLSA build-provenance, SBOM SPDX atestada, atestação SLSA do knowledge pack, GitHub Action composite, tag móvel v0 via tag-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 em release.yml/action.yml/.goreleaser.yaml — ver 10-infraestrutura.md para a fonte autoritativa.
  4. O conteúdo dos hubs de crosswalk (AVD para aws/azure/gcp.yaml, C-#### para k8s.yaml, cobertura de controles) foi tomado do briefing e da presença dos arquivos em crosswalk/; o mecanismo de carga (crosswalk.Load, schemaVersion, fallback /opt/quorum/crosswalk) foi verificado no código.
  5. O diagrama de sequência representa o caminho feliz com Correlator não-nulo e --offline desligado; variações (offline, correlator nil, scanner indisponível, conftest sem Rego → error e a camada de advice degradando quando o modelo está inacessível) estão descritas em texto.