Ir para o conteúdo

05 — Modelagem de Dados

Este documento descreve o modelo de dados do Quorum (quorum-sec-scan, v0.8.3 — revisão 2026-07-04). O Quorum é uma ferramenta CLI/Docker de consensus security scanning: ela orquestra um pool de scanners OSS (trivy, grype, checkov, kics, dockle, kubescape, polaris, kube-score, terrascan, tfsec, regula, conftest), normaliza toda a saída para um modelo de domínio canônico em memória, resolve aliases de vulnerabilidade, correlaciona findings equivalentes por um correlationKey determinístico, calcula um score de confiança (consenso) e serializa o resultado em SARIF (primário), JSON ou XML.

Como o Quorum não possui banco de dados (ver §1), "modelo de dados" aqui significa o modelo de domínio: um conjunto de structs Go imutáveis durante o pipeline, com três projeções de persistência (SARIF/JSON/XML) geradas a cada execução. Este documento cobre os três níveis clássicos de modelagem — Conceitual, Lógico e Físico — adaptados a esse contexto, com diagramas Mermaid, invariantes e checklists.

Fontes verificadas no código: internal/model/model.go, internal/correlate/key.go, internal/correlate/correlate.go, internal/crosswalk/crosswalk.go, crosswalk/{aws,azure,gcp,k8s}.yaml, internal/consensus/consensus.go, internal/orchestrator/orchestrator.go, internal/report/{sarif,json,xml}.go, internal/severity/severity.go, internal/purl/purl.go, internal/adapter/. Tipos da camada de aconselhamento verificados em internal/model/model.go (ver §8).


1. Não há RDBMS — justificativa

O Quorum não usa banco de dados relacional (PostgreSQL, MySQL, SQLite etc.), nem NoSQL, nem qualquer datastore persistente de domínio. Isso é uma decisão arquitetural, não uma lacuna.

Critério Realidade do Quorum
Topologia de execução Processo CLI efêmero (ou container Docker), uma execução = uma varredura
Ciclo de vida dos dados Todos os findings vivem em memória ([]model.Finding) durante uma única execução e são serializados para arquivo/stdout ao final
Estado entre execuções Stateless por design. Não há histórico, usuários, sessões ou multi-tenancy
Persistência existente Apenas caches de arquivo: aliases (~/.cache/quorum/aliases.json, perm 0600, com schemaVersion) e DB do grype pré-cacheado na imagem :full (com GRYPE_DB_VALIDATE_AGE=false, não expira) — caches reconstruíveis, não fonte de verdade. A camada de aconselhamento adiciona um cache opcional em disco (--advice-cache, ver §8), também reconstruível
Concorrência de escrita Inexistente — não há writers concorrentes a coordenar; o paralelismo é fan-out de leitura de scanners (goroutines)

Por que um RDBMS seria a ferramenta errada:

  1. Sem entidades de longa duração. Um banco serve para guardar estado que sobrevive a processos e é consultado/atualizado por múltiplos clientes. O Quorum produz um relatório e termina. O "estado" é o arquivo SARIF/JSON/XML.
  2. A integração natural é o ecossistema de CI/segurança. O destino dos dados é o GitHub Code Scanning (via SARIF), artefatos de pipeline e gates de exit code — não dashboards consultando um banco. (Opcionalmente, --metrics emite um arquivo Prometheus textfile para telemetria — ainda assim um artefato de saída, não um datastore.)
  3. Reprodutibilidade. O correlationKey/Fingerprint são determinísticos e puros (função do conteúdo do finding). A "chave primária" do domínio é derivada, não auto-incremento de um banco.
  4. Operação simples e auditável. Sem banco não há migrations, connection pools, backups ou superfície de ataque de dados em repouso. Alinha com o princípio de supply chain mínima do projeto.

Proposta futura (claramente separada, NÃO implementada): se um dia surgir necessidade de histórico/tendências (ex.: "este CVE reaparece há 3 releases"), o caminho idiomático seria ingerir os SARIF/JSON já emitidos em um store externo (ex.: o próprio GitHub Advanced Security, um data lake, ou um SQLite append-only de séries temporais) fora do binário Quorum. O núcleo permaneceria stateless. Isso é uma ideia, não um compromisso de roadmap.


2. Modelo Conceitual

No nível conceitual, o domínio do Quorum descreve o que é representado, independentemente de Go ou de formato de arquivo.

2.1 Entidades de domínio

Entidade Significado conceitual
Target O artefato sendo varrido: imagem de container, repositório/IaC ou manifestos/cluster k8s
Scanner (Adapter) Um motor OSS que inspeciona o Target e produz achados nativos
ScannerRun O registro de o que aconteceu com um scanner em uma execução (rodou? falhou? indisponível?) — transparência: "0 vulns nunca pode parecer 'não rodou'"
Finding A unidade canônica de achado, normalizada de qualquer scanner para um único formato
CorrelationKey / Fingerprint A identidade determinística de um achado, usada para agrupar achados equivalentes de scanners diferentes
MergedFinding O resultado de consenso: um grupo de Findings equivalentes, com severidade agregada e score de confiança
Report A projeção serializada (SARIF/JSON/XML) do conjunto de MergedFindings + ScannerRuns

2.2 Diagrama conceitual (ER)

erDiagram
    TARGET ||--o{ SCANNER_RUN : "é varrido por"
    SCANNER_RUN ||--o{ FINDING : "produz (se status=ran)"
    FINDING }o--|| CORRELATION_KEY : "recebe identidade"
    CORRELATION_KEY ||--|| MERGED_FINDING : "agrupa em"
    MERGED_FINDING ||--o{ FINDING : "contém (Members)"
    RESULT ||--o{ SCANNER_RUN : "agrega"
    RESULT ||--o{ MERGED_FINDING : "agrega"
    RESULT ||--|| REPORT : "serializa como SARIF/JSON/XML"

    TARGET {
        string Type "image|repo|k8s"
        string Ref  "ref de imagem ou caminho"
    }
    SCANNER_RUN {
        string Name
        string Version
        string Status "ran|skipped|unavailable|error|timeout"
        int    Findings
        int    DurationMs
        string Error
    }
    FINDING {
        string Type "VULN|MISCONFIG|SECRET|K8S_POSTURE|IMG_HARDENING"
        string Scanner
        string VulnID
        string PURL
        string CanonicalControl
        string Severity
        string CorrelationKey
        string Fingerprint
    }
    MERGED_FINDING {
        string CorrelationKey PK
        string Severity "agregada (max)"
        int    DetectionCount
        float  Confidence "0..1"
        string Fingerprint
    }

2.3 Princípio central: false split > false merge

A regra de ouro do domínio é que separar erroneamente dois achados distintos é preferível a fundir erroneamente achados diferentes. Isso permeia todo o modelo de identidade (correlationKey), o crosswalk (que só agrupa rule-ids quando a equivalência é clara, ver §4.5) e o consenso: na dúvida, o correlationKey é mais específico, e findings Unmapped nunca se fundem silenciosamente com outros (ver §4.4).


3. Modelo Lógico

O nível lógico descreve atributos, tipos, cardinalidades, invariantes e relacionamentos — ainda sem se prender à sintaxe Go.

3.1 Diagrama de classes

classDiagram
    direction LR

    class Result {
        +Target Target
        +ScannerRun[] Runs
        +Finding[] Findings  "raw, não serializado"
        +MergedFinding[] Merged
        +Time StartedAt
        +Duration Duration
    }

    class Target {
        +TargetType Type
        +string Ref
    }

    class ScannerRun {
        +string Name
        +string Version
        +string Status
        +int Findings
        +Duration Duration
        +string Error
    }

    class Finding {
        +FindingType Type
        +string Scanner
        +string ScannerVersion
        +string VulnID
        +string[] Aliases
        +string PURL
        +string RuleID
        +string CanonicalControl
        +string Category
        +bool Unmapped
        +Resource Resource
        +Location Location
        +Severity Severity
        +float64 CVSS
        +string CorrelationKey
        +string Fingerprint
        +string Title
        +string Description
        +bool Confirmed
        +map Raw "não serializado"
    }

    class MergedFinding {
        +string CorrelationKey
        +FindingType Type
        +string Title
        +Severity Severity
        +string[] DetectedBy
        +int DetectionCount
        +float64 Confidence
        +bool Unmapped
        +Finding[] Members
        +string Fingerprint
        +Remediation Remediation  "aconselhamento, opt-in"
        +DocRef[] References      "aconselhamento, opt-in"
        +Advice Advice            "aconselhamento, opt-in"
    }

    class Resource {
        +string Kind
        +string Name
        +string Namespace
        +string Address
    }

    class Location {
        +string File
        +int StartLine
        +int EndLine
        +string ImageLayer
    }

    class FindingType {
        <<enumeration>>
        VULN
        MISCONFIG
        SECRET
        K8S_POSTURE
        IMG_HARDENING
    }

    class Severity {
        <<enumeration>>
        CRITICAL
        HIGH
        MEDIUM
        LOW
        INFO
        UNKNOWN
        +Rank() int
    }

    Result "1" *-- "1" Target
    Result "1" *-- "0..*" ScannerRun
    Result "1" *-- "0..*" Finding
    Result "1" *-- "0..*" MergedFinding
    Finding "1" *-- "1" Resource
    Finding "1" *-- "1" Location
    Finding "1" --> "1" FindingType
    Finding "1" --> "1" Severity
    MergedFinding "1" o-- "1..*" Finding : Members
    MergedFinding "1" --> "1" FindingType
    MergedFinding "1" --> "1" Severity
    MergedFinding "1" o-- "0..1" Remediation
    MergedFinding "1" o-- "0..*" DocRef : References
    MergedFinding "1" o-- "0..1" Advice

3.2 Enumerações

FindingType — classifica e seleciona a estratégia de correlação

O conjunto de valores do FindingType permanece inalterado desde a v0.2.3 (cinco valores). O que mudou foi o pool de scanners que alimenta cada tipo: com 12 adapters, mais engines produzem MISCONFIG e K8S_POSTURE.

Valor Significado Produzido por (típico)
VULN Vulnerabilidade de software (SCA) trivy, grype
MISCONFIG Má-configuração de IaC trivy, checkov, kics, terrascan, tfsec, regula, conftest (policy-as-code)
SECRET Segredo exposto trivy (com redaction do Match do segredo)
K8S_POSTURE Postura de segurança Kubernetes kubescape, polaris, kube-score
IMG_HARDENING Endurecimento de imagem de container dockle

FindingType é o discriminador que seleciona o algoritmo de BuildKey (ver §4). Note que conftest é policy-as-code: avalia o seu Rego (de ./policy) e emite MISCONFIG; sua identidade cai no ramo MISCONFIG do BuildKey como qualquer outra IaC.

Severity — escala normalizada única

Valor Rank() Origem da normalização
CRITICAL 5 CVSS ≥ 9.0; labels "CRITICAL"/"CRIT"
HIGH 4 CVSS ≥ 7.0; "HIGH"/"ERROR"/"DANGER"; Dockle "FATAL"
MEDIUM 3 CVSS ≥ 4.0; "MEDIUM"/"MODERATE"/"WARNING"; Dockle "WARN"
LOW 2 CVSS > 0; "LOW"/"MINOR"; Dockle "INFO"
INFO 1 "INFO"/"INFORMATIONAL"/"NEGLIGIBLE"; Dockle "PASS/SKIP/IGNORE"
UNKNOWN 0 CVSS = 0; "UNKNOWN"/"NONE"/vazio; rótulo não reconhecido

A normalização vive em internal/severity/severity.go: FromCVSS, FromLabel, FromDockle, mais utilitários Max, AtLeast (usado por --fail-on/--min-severity) e Parse. Rank() torna a escala ordenável para agregação e ordenação de relatório.

3.3 Atributos de Finding (entidade central)

Atributo Tipo lógico Obrigatório Notas
Type FindingType Sim Discriminador de correlação
Scanner string Sim Nome do adapter de origem
ScannerVersion string Não Versão do binário
VulnID string Condicional (VULN) CVE/GHSA; canônico após resolução de alias
Aliases string[] Não Outros ids reportados pelo scanner
PURL string Condicional (VULN) pkg:type/ns/name@version
RuleID string Condicional (MISCONFIG/etc.) Id nativo da regra; entrada do crosswalk (scanner\|ruleID)
CanonicalControl string Não Resultado do crosswalk (AVD/CWE/CIS) — ou já nativo (trivy/tfsec emitem AVD)
Category string Não Categoria semântica (fallback do crosswalk)
Unmapped bool Não true quando o crosswalk não resolveu controle
Resource Resource Não Objeto IaC/k8s alvo
Location Location Não Arquivo/linha ou layer de imagem
Severity Severity Sim Escala normalizada
CVSS float64 Não 0 = ausente
CorrelationKey string Computado Atribuído pelo correlator, nunca pelo adapter
Fingerprint string Computado sha256(CorrelationKey)
Title string Sim Título legível
Description string Não
Confirmed bool Não Confirmado por fonte autoritativa (NVD/OSV)
Raw map[string]any Não Payload original; não serializado (tag json:"-")

3.4 Relacionamentos e cardinalidades

Relacionamento Cardinalidade Regra
Result → ScannerRun 1 : 0..N Um run por adapter selecionado (mesmo se skipped/unavailable)
ScannerRun → Finding 1 : 0..N status=ran produz findings; demais produzem 0
Finding → CorrelationKey N : 1 Muitos findings podem compartilhar a mesma chave
CorrelationKey → MergedFinding 1 : 1 Cada chave distinta vira exatamente um MergedFinding
MergedFinding → Finding (Members) 1 : 1..N Sempre ≥ 1 membro (grupo nunca vazio)
Finding → Resource / Location 1 : 1 Value objects embutidos (sempre presentes, possivelmente vazios)
MergedFinding → Remediation / Advice 1 : 0..1 Aconselhamento, opt-in; nil a menos que o enriquecimento --advice tenha rodado (ver §8)
MergedFinding → DocRef (References) 1 : 0..N Aconselhamento, opt-in; vazio a menos que o enriquecimento --advice tenha rodado

3.5 Invariantes e constraints lógicas

Estas são as restrições que substituem as constraints de um RDBMS. Não há um motor que as imponha em uma tabela — elas são garantidas por construção no código e/ou verificadas por testes de contrato/unitários (cobertura de testes exigida no CI).

  • INV-1 (identidade computada). Adapters nunca preenchem CorrelationKey/Fingerprint. Esses campos são atribuídos centralmente em Correlator.Enrich (ou no fallback do orchestrator). Origem: comentário em model.go e orchestrator.go.
  • INV-2 (Fingerprint deriva da chave). Sempre Fingerprint == sha256hex(CorrelationKey). Função pura, sem sal. Origem: correlate.Fingerprint.
  • INV-3 (severidade agregada = máximo). MergedFinding.Severity == max(Members[*].Severity) por Rank(). Origem: consensus.aggregateSeverity.
  • INV-4 (contagem por scanner distinto). DetectionCount == |distinct(lower(Members[*].Scanner))|, não o número de findings — dois findings do mesmo scanner contam 1. Origem: consensus.distinctScanners/DetectionCount.
  • INV-5 (DetectedBy ordenado e único). Lista de scanners distintos, lowercased e ordenada alfabeticamente. Origem: distinctScanners (usa sort.Strings).
  • INV-6 (confiança limitada). 0 ≤ Confidence ≤ 1 (via clamp01). Origem: consensus.confidence.
  • INV-7 (propagação de Unmapped). MergedFinding.Unmapped == OR(Members[*].Unmapped). Origem: consensus.anyUnmapped.
  • INV-8 (não-fusão de unmapped). Um finding Unmapped é chaveado por UNMAPPED:<lower(scanner)>:<UPPER(ruleID)>, garantindo que nunca se funda silenciosamente a outro de controle diferente. Origem: correlate.controlKey.
  • INV-9 (grupo não-vazio). Todo MergedFinding tem len(Members) ≥ 1; Type, Fingerprint herdam de Members[0]. Origem: consensus.Merge.
  • INV-10 (Title resolvido). MergedFinding.Title é o primeiro título não-vazio entre os membros; fallback para o CorrelationKey. Origem: consensus.bestTitle.
  • INV-11 (ordenação determinística do relatório). MergedFindings são ordenados por (Severity.Rank desc, Confidence desc, DetectionCount desc, CorrelationKey asc). O critério final por CorrelationKey garante estabilidade total. Origem: consensus.Merge (sort.SliceStable).
  • INV-12 (transparência de scanner). Todo adapter selecionado gera um ScannerRun com Status ∈ {ran, skipped, unavailable, error, timeout}, mesmo sem findings. Origem: orchestrator.runOne.
  • INV-13 (aconselhamento é presentation-only). Remediation/References/Advice são preenchidos depois do consenso e nunca alimentam CorrelationKey/Fingerprint/Confidence/severidade agregada ou o gate --fail-on. Sem --advice permanecem nil/vazios e a saída é byte-idêntica. Origem: comentários de campo em model.go; pacotes internal/advisor/internal/enrich.

Checklist de validação de invariantes (para revisão/PR)

  • [ ] Nenhum adapter atribui CorrelationKey ou Fingerprint (INV-1)
  • [ ] Fingerprint confere com sha256hex(CorrelationKey) (INV-2)
  • [ ] Severity do merge é o máximo dos membros (INV-3)
  • [ ] DetectionCount = scanners distintos, não findings (INV-4)
  • [ ] Confidence permanece em [0,1] (INV-6)
  • [ ] Findings Unmapped jamais co-agrupados com controles diferentes (INV-8)
  • [ ] Ordenação do relatório é totalmente determinística (INV-11)
  • [ ] Todo scanner selecionado aparece em Runs com status válido (INV-12)
  • [ ] Campos de aconselhamento nunca alteram identidade, confiança ou o gate; ausentes sem --advice (INV-13)

4. correlationKey e Fingerprint (identidade)

A "chave primária" do domínio é o correlationKey, uma função pura e determinística do conteúdo normalizado do finding, definida em internal/correlate/key.go. Não há chave universal: cada FindingType tem sua estratégia, refletindo o fato de que "equivalência" significa coisas diferentes para um CVE e para uma má-config de Terraform.

4.1 Pipeline de identidade

flowchart LR
    A["Finding normalizado<br/>(do adapter)"] --> B{Type?}
    B -->|VULN| C["resolveVuln:<br/>VulnID -> canônico (alias/OSV)"]
    B -->|MISCONFIG / K8S / IMGH| D["resolveControl:<br/>crosswalk -> CanonicalControl<br/>ou Unmapped=true"]
    C --> E["BuildKey(f)"]
    D --> E
    E --> F["CorrelationKey"]
    F --> G["Fingerprint = sha256(CorrelationKey)"]
    G --> H["consensus.Merge:<br/>agrupa por CorrelationKey"]

Enriquecimento e keying acontecem em Correlator.Enrich; o agrupamento é responsabilidade do pacote consensus. O pacote correlate "owns identity", o consensus "owns grouping". Em resolveControl, findings que já chegam com CanonicalControl preenchido (trivy e tfsec emitem ids AVD nativamente) não passam pelo crosswalk — a auto-correlação com o trivy é imediata.

4.2 Fórmulas de BuildKey por tipo

Type Fórmula do correlationKey Estabilidade-alvo
VULN VULN\|<UPPER(VulnID)>\|<purl.NameVersion(PURL)> Mesmo CVE + mesmo name@version correlaciona entre scanners, ignorando prefixo de ecossistema/qualificadores
MISCONFIG MISCONFIG\|<fileKey>\|<resourceType>\|<controlKey> fileKey = basename minúsculo; resourceType derivado do address (ex.: aws_s3_bucket)
K8S_POSTURE K8S\|<objectRef>\|<controlKey> objectRef = ns/kind/name. O container NÃO entra na chave (ver nota abaixo)
IMG_HARDENING IMGH\|<controlKey> Endurecimento é global à imagem
SECRET SECRET\|<normPath>\|<lineKey>\|<lower(RuleID)> Caminho normalizado + linha + regra
(default) OTHER\|<Scanner>\|<Title> Fallback conservador

Mudança desde a v0.2.3 — chave K8S sem container. A fórmula de K8S_POSTURE deixou de incluir o Address (container). O comentário em key.go explica o motivo: os engines reportam em granularidades diferentes — o kubescape no nível do workload, o polaris por container. Incluir o container na chave bloquearia o consenso entre engines. Trade-off aceito conscientemente: dois containers que falham o mesmo controle no mesmo workload fundem em um único finding — adequado para postura (false split > false merge).

4.3 Normalização de componentes

Helper O que faz Por quê
purl.NameVersion Tira pkg:, qualificadores ?/#; minúsculo Mesmo pacote correlaciona apesar de variações de ecossistema
fileKey path.Base + minúsculo Scanners reportam raízes/relatividade diferentes; só o basename é estável
normPath Normaliza separadores, path.Clean, remove ./,/, minúsculo Caminho estável para SECRET
resourceType Primeiro segmento do address contendo _ (ex.: aws_s3_bucket); fallback para Kind Engines discordam entre address Terraform e nome literal do recurso
objectRef lower(namespace/kind/name), namespace vazio → default Identidade estável de objeto k8s (nível de workload, sem container)
lineKey StartLine (ou 0 se ausente) Discrimina segredos na mesma regra/arquivo

4.4 controlKey e tratamento de Unmapped

controlKey prefere CanonicalControl (maiúsculo). Quando não há controle resolvido, faz fallback para UNMAPPED:<lower(scanner)>:<UPPER(RuleID)>. Isso materializa o princípio false split > false merge: um finding sem mapeamento canônico mantém uma identidade própria e nunca colide com outro de controle distinto.

Trade-off documentado (KNOWN ISSUE no código): para MISCONFIG, dois recursos distintos do mesmo tipo com o mesmo controle no mesmo arquivo podem super-fundir. Foi aceito conscientemente como preferível a nunca correlacionar entre engines.

4.5 Crosswalk: hubs canônicos e formato versionado

O CanonicalControl que alimenta o controlKey vem do crosswalk (internal/crosswalk/crosswalk.go): um mapeamento scanner + ruleID → controle canônico carregado dos arquivos YAML em ./crosswalk. Todos os mapeamentos foram DERIVADOS de output real dos scanners (rodados via imagem :full contra examples/), pareando ids somente onde os engines sinalizam o mesmo conceito no mesmo recurso.

Hubs canônicos por domínio:

Arquivo Hub (id canônico) Cobertura Engines pareados
crosswalk/aws.yaml AVD (AVD-AWS-####) S3, IAM, EBS, Security Group, RDS, KMS, CloudTrail, VPC flow logs trivy (nativo), checkov, kics, terrascan, regula
crosswalk/azure.yaml AVD (AVD-AZU-####) Storage account (HTTPS/TLS), Key Vault trivy (nativo), checkov, kics, terrascan, regula
crosswalk/gcp.yaml AVD (AVD-GCP-####) GCS bucket, firewall (RDP/SSH), Cloud SQL trivy (nativo), checkov, kics, terrascan, regula
crosswalk/k8s.yaml Kubescape (C-####) privilege-escalation, privileged, non-root, cpu/mem limits, probes, read-only-fs, linux-hardening, automount-SA, network-policy, host-network, host-PID/IPC, capabilities, secrets kubescape (nativo), polaris, kube-score

Notas de design fiéis ao código:

  • Trivy e tfsec são o hub de IaC. O trivy fala AVD nativamente; o tfsec também emite ids AVD e por isso auto-correlaciona com o trivy sem precisar de entrada no crosswalk (o resolveControl retorna cedo quando CanonicalControl já está preenchido).
  • RBAC segue single-engine. No crosswalk/k8s.yaml, controles de RBAC do kubescape (C-0035/C-0185/C-0272/…) não são pareados: dependem de contexto de cluster ativo e não dispararam sobre manifestos estáticos, então ficam Unmapped em vez de forçar um false merge. Documentado no próprio YAML.
  • Checks agrupados do kube-score (container-resources = cpu+memory, pod-probes = liveness+readiness) são deixados intencionalmente sem mapeamento — mapear um check agrupado a um único controle canônico causaria over-merge.

Formato versionado (schemaVersion)

O loader (crosswalk.Load) aceita duas formas de arquivo, tentando a versionada primeiro:

  1. Documento versionado (preferido): mapa com schemaVersion: 1 + controls: [...]. Usado por azure.yaml, gcp.yaml e k8s.yaml. A constante crosswalk.SchemaVersion = 1 define a versão corrente e permite evoluir o formato.
  2. Lista simples (legado, ainda suportado): o YAML é uma lista de controles no topo (sem schemaVersion). Usado hoje por aws.yaml.

Cada Control tem canonicalControl, category, cwe (opcional), title e ids: { scanner: [ruleID, ...] }. O índice interno é byRule["<lower(scanner)>|<ruleID>"] → Resolution. Um diretório de crosswalk ausente não é erro (o Quorum roda sem crosswalk custom; todos os findings degradam para Unmapped).


5. Consenso e MergedFinding

O consensus.Merge (internal/consensus/consensus.go) agrupa Findings por CorrelationKey e produz MergedFindings pontuados. Contagem bruta de detecções não é confiança — diversidade de engine, severidade e confirmação autoritativa também pesam (DESIGN §9).

5.1 Fórmula de Confidence

confidence = clamp01(
      0.35 * count
    + 0.25 * diversity
    + 0.25 * severity
    + 0.15 * authoritative
)
Componente Cálculo Faixa
count log(1+N) / log(5), N = scanners distintos (retornos decrescentes) ~0..1
diversity famílias de engine distintas: 1→0.33, 2→0.66, 3+→1.0 0..1
severity CRITICAL=1.0, HIGH=0.8, MEDIUM=0.5, LOW=0.3, INFO/UNKNOWN=0.1 0.1..1
authoritative 1.0 se algum membro Confirmed, ou for CVE com CVSS>0; senão 0 0 ou 1

Famílias de engine (scannerCategory), atualizadas para os 12 scanners: trivy/grypesca; checkov/kics/terrascan/tfsec/regulaiac; conftestpolicy; kubescape/polaris/kube-scorek8s; docklehardening. Scanners desconhecidos viram other:<nome>, contando como família própria (diversidade não infla artificialmente). A diversidade recompensa consenso entre famílias distintas: dois engines da mesma família (ex.: checkov + kics, ambos iac) somam menos que engines de famílias diferentes.

5.2 Diagrama de derivação do MergedFinding

flowchart TD
    G["Grupo: Finding[] com mesma CorrelationKey"] --> S["distinctScanners -> DetectedBy, DetectionCount"]
    G --> A["aggregateSeverity -> Severity (max)"]
    G --> T["bestTitle -> Title"]
    G --> U["anyUnmapped -> Unmapped"]
    S --> C["confidence(members, scanners, agg) -> Confidence"]
    A --> C
    G --> M["MergedFinding"]
    S --> M
    A --> M
    T --> M
    U --> M
    C --> M

Nota de aconselhamento: quando --advice está habilitado, uma etapa de enriquecimento roda depois do Merge e pode anexar Remediation/References/Advice a cada MergedFinding. Essa etapa lê o merged finding mas nunca muta sua identidade, severidade ou confiança (INV-13). Ver §8.


6. Modelo Físico

No nível físico, o "armazenamento" do Quorum é (a) o layout de structs Go em memória e (b) as três projeções serializadas. Não há schema SQL.

6.1 Físico em memória — structs Go

As definições canônicas estão em internal/model/model.go. Pontos físicos relevantes:

  • Finding.Raw map[string]any tem tag json:"-"nunca serializado; existe só para depuração/adapters.
  • Result.Findings tem tag json:"-" → os findings brutos não vão para o relatório padrão; o relatório expõe Merged (renomeado para findings no JSON). Origem: orchestrator.Result.
  • Campos com omitempty desaparecem quando vazios (ex.: aliases, purl, cvss, description). Os campos de aconselhamento remediation, references, advice são todos omitempty → ausentes da saída a menos que --advice os tenha preenchido.
  • Severity e FindingType são string typedefs — serializam como strings literais ("CRITICAL", "VULN").

6.2 Físico serializado — três projeções

Formato Reporter Papel Forma
SARIF 2.1.0 sarif.go Primário — GitHub Code Scanning runs[].results[] com partialFingerprints["quorum/v1"]
JSON json.go Integração genérica / detalhe {tool, version, target, scanners, summary, findings} (findings = dump de []MergedFinding)
XML xml.go Pipelines legados/JUnit-like <quorumReport> espelhando o JSON

Hardening do caminho de saída: --output aplica filepath.Clean e grava com permissão 0600; um target iniciando com - é recusado (proteção contra argument injection); ids de OSV são validados e passam por url.PathEscape. Não altera o modelo de dados, mas afeta como as projeções são escritas.

6.2.1 Mapeamento canônico → SARIF

Campo canônico Destino SARIF
MergedFinding.Fingerprint result.partialFingerprints["quorum/v1"]
Severity result.level (CRITICAL/HIGH→error, MEDIUM→warning, resto→note)
Title result.message.text + rule.shortDescription
Type rule.name + rule.properties.type
VulnID / CanonicalControl / RuleID result.ruleId (nessa ordem de preferência; fallback CorrelationKey)
Location.File/StartLine/EndLine result.locations[].physicalLocation (deduplicado por arquivo)
DetectedBy, DetectionCount, Confidence (arredondado 2 casas), Severity, CorrelationKey, Unmapped result.properties
Result.Target.Ref + resumo de scanners run.properties

Nota de versão: report.Version (namespace do fingerprint e versão do driver SARIF) está fixado em "0.1.0" no código (sarif.go linha 13), distinto da versão do produto (v0.8.3). Ver Gaps.

6.2.2 Estrutura do JSON

{
  "tool": "quorum",
  "version": "0.1.0",
  "target": { "type": "repo", "ref": "./" },
  "scanners": [
    { "name": "trivy", "version": "0.55.0", "status": "ran",
      "findings": 12, "durationMs": 1842 }
  ],
  "summary": {
    "totalFindings": 7,          // = len(Merged)
    "durationMs": 5300,
    "bySeverity": { "CRITICAL": 1, "HIGH": 3 },
    "multiDetected": 2           // findings com detectionCount > 1
  },
  "findings": [ /* []MergedFinding */ ]
}

Cada elemento de findings é um MergedFinding. Sob --advice, ele pode carregar os objetos opcionais remediation, references e advice (todos omitempty); sem --advice essas chaves ficam ausentes e o JSON é byte-idêntico ao de uma execução simples.

6.2.3 Estrutura do XML

<quorumReport tool="quorum" version="..."> com <target>, <scanners><scanner .../></scanners> e <findings><finding>...</finding></findings>. Cada <finding> carrega type, severity, detectionCount, confidence, unmapped, fingerprint como atributos, e correlationKey, title, detectedBy>scanner, locations>location como elementos.

6.3 Índices, Triggers, Views, Procedures, Sequences — N/A

Construto Status Justificativa
Índices N/A Não há banco a indexar. Os "índices" lógicos são construídos em memória por execução e descartados ao fim: o map[string][]Finding por CorrelationKey em consensus.Merge e o byRule map[string]Resolution do crosswalk (acesso O(1) amortizado)
Triggers N/A Sem banco e sem mutações persistentes. A "reação a evento" análoga é o pipeline funcional puro scan → normalize → alias → correlate → score → report
Views N/A As três serializações (SARIF/JSON/XML) são as "views" de leitura do mesmo conjunto canônico, geradas em tempo de execução pelos reporters
Stored Procedures / Functions N/A Toda a lógica é código Go (ex.: BuildKey, confidence). Não há SQL/PL nem RPC de banco. A exceção controlada é o policy-as-code do conftest, que roda Rego do usuário (./policy) — mas produz Findings, não persiste estado
Sequences / Auto-increment N/A A identidade é derivada do conteúdo (Fingerprint = sha256(CorrelationKey)), não gerada por sequência
Constraints (FK/UNIQUE/CHECK) N/A no SGBD; equivalentes no código As FKs/uniques são garantidas por construção: agrupamento por CorrelationKey (UNIQUE lógico), Members ⊆ Findings (FK lógica). Os CHECKs equivalentes são as invariantes da §3.5
Migrations N/A Sem schema persistente. Mudanças de modelo são mudanças de struct + versionamento de release; compatibilidade externa é responsabilidade do contrato SARIF/JSON/XML. O único artefato versionado por schema é o crosswalk (schemaVersion: 1), que evolui por leitura tolerante (aceita forma legada e versionada), não por migration

Proposta futura (NÃO implementada): caso o ingest externo descrito na §1 seja adotado, índices em (fingerprint) e (vulnId) e views materializadas de tendência fariam sentido — no store externo, jamais no binário.


7. Dicionário de dados (resumo)

Struct Pacote Papel Serializado em
Finding model Unidade canônica normalizada JSON (dentro de members), SARIF (origem dos campos)
MergedFinding model Grupo pontuado pós-consenso SARIF/JSON/XML (unidade de relatório)
Resource model Objeto IaC/k8s alvo embutido
Location model Arquivo/linha/layer SARIF locations, XML locations
Severity model Escala normalizada (enum) string
FindingType model Discriminador de correlação (enum) string
Remediation model Template determinístico de remediação (Fase 0, aconselhamento) JSON remediation (opt-in, omitempty)
DocRef model Referência externa OWASP (aconselhamento) JSON references (opt-in, omitempty)
Advice model Recomendação opt-in de LLM local, sempre rotulada (aconselhamento) JSON advice (opt-in, omitempty)
Fix model Patch proposto por IA e verificado, embutido em Advice (aconselhamento) JSON advice.fix (opt-in, omitempty)
Control / Resolution crosswalk Mapeamento scanner+ruleID → controle canônico não serializado (config de entrada, crosswalk/*.yaml)
ScannerRun orchestrator Transparência de execução JSON/XML scanners, SARIF run.properties
Result orchestrator Saída completa do scan base de todos os reporters
Target adapter Artefato varrido target em todos os formatos

8. Camada de aconselhamento no modelo (opt-in)

Desde a v0.8.3, o MergedFinding carrega uma camada de aconselhamento opcional, habilitada pela flag --advice. Esses campos são presentation-only: são preenchidos depois do consenso e nunca tocam CorrelationKey, Fingerprint, Confidence, severidade agregada ou o gate --fail-on. Sem --advice, permanecem nil/vazios e o relatório é byte-idêntico ao de uma execução simples (INV-13). O núcleo determinístico continua sem IA; as partes de IA são estritamente opt-in e desligadas por padrão.

8.1 Novos campos em MergedFinding

Campo Tipo Preenchido por Significado
Remediation *Remediation Fase 0 (determinística, sem modelo) Template de remediação curado, casado por canonicalControl/ruleId/category/type
References []DocRef Fase 0 / Fase 2 (determinística) Referências OWASP; a Fase 2 as recupera de um corpus OWASP com digest fixado
Advice *Advice Fase 1 (LLM local opt-in) Recomendação em linguagem natural de um endpoint OpenAI-compatível no host; sempre rotulada

8.2 Value objects de aconselhamento

classDiagram
    class Remediation {
        +string Title
        +string Summary
        +string Snippet
        +string Source  "template — determinístico, nunca adivinhado"
    }
    class DocRef {
        +string Title
        +string URL
    }
    class Advice {
        +string Recommendation
        +Fix Fix
        +string Provider  "local | remote"
        +string Model
        +string Label     "AI-generated, advisory only"
    }
    class Fix {
        +string File
        +string Content   "substituição completa proposta"
        +bool Verified    "sempre true quando anexado"
    }
    Advice "1" o-- "0..1" Fix
  • model.Remediation — uma correção sugerida. Na Fase 0 é um template curado e determinístico, chaveado pelo controle canônico do finding (Source = "template", nunca adivinhado). Snippet é ilustrativo, não um diff exato. Os dados vivem em knowledge/*.yaml (aws/azure/gcp/k8s/image/categories); pacote internal/enrich.
  • model.DocRef — uma referência externa (ex.: um OWASP cheat sheet) que ancora a remediação em uma fonte autoritativa e citável. A Fase 2 (RAG-as-artifact, pacote internal/rag) as recupera deterministicamente do corpus OWASP versionado e com digest fixado (knowledge/owasp/corpus.yaml): recuperação lexical por padrão (sem modelo), semântica (embeddings) quando o corpus é embedado via quorum advise-index — o scan escolhe semântica automaticamente quando o corpus tem vetores.
  • model.Advice — a camada de aconselhamento gerada por IA (Fase 1, opt-in via --advice-provider=local; Fase 3 via =remote). Provider é local ou remote, Model é o id que o endpoint reportou e Label é sempre "AI-generated, advisory only". 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 o conselho de IA e o scan nunca falha. Pacote internal/advisor.
  • model.Fix — uma substituição do arquivo problemático proposta por IA, anexada apenas quando --fix=suggest está setado e ela passou pelo laço de verify-the-fix: o patch é aplicado a uma cópia temporária, re-escaneado com o mesmo scanner e mantido apenas se o finding sumiu e o arquivo ainda faz parse. Verified é sempre true quando anexado (caso contrário o fix é descartado). O Quorum nunca aplica automaticamente. O provider remoto recusa --fix (faria upload de código-fonte) e é bloqueado por --offline; apenas o finding normalizado é enviado a um provider remoto — nunca código-fonte.

8.3 Serialização

Os três campos carregam omitempty, então ficam ausentes do JSON/XML a menos que o enriquecimento os tenha preenchido. Novas métricas (só sob --advice) refletem os mesmos dados: quorum_advice_enriched{kind=remediation|references|recommendation}, quorum_advice_provider{provider} e quorum_advice_fix{stage=proposed|verified} (a taxa de verify-the-fix). O knowledge pack + crosswalk ganham uma atestação de proveniência de build SLSA a cada release (verifique com gh attestation verify knowledge/owasp/corpus.yaml). Ver 13-ia.md para o enquadramento honesto e 21-proposta-ia.md para o design completo das Fases 0–3.


Premissas

  • Versão do código analisada: estado do repositório na branch main no momento da redação (produto rotulado v0.8.3, revisão 2026-07-04). As citações refletem os arquivos lidos diretamente (key.go, correlate.go, crosswalk.go, consensus.go, model.go, os crosswalk/*.yaml); nenhum comportamento foi inferido sem leitura.
  • Modelo canônico inalterado no núcleo. As structs de domínio e o FindingType (5 valores) permanecem idênticos à v0.2.3; o que evoluiu foi o pool de scanners (6 → 12), o crosswalk (agora com hubs AVD/kubescape versionados), a chave K8S (sem container) e — novo na v0.8.3 — os campos de aconselhamento opt-in no MergedFinding (Remediation/References/Advice), que são presentation-only e desligados por padrão.
  • report.Version = "0.1.0" foi reportado fielmente como está no código. Assumiu-se que essa string é a versão do contrato de saída/namespace de fingerprint, não a versão do produto — ver Gaps.
  • DESIGN.md é referenciado por seções (§3, §6, §8, §9) conforme comentários no código; o conteúdo detalhado desse documento não foi relido aqui, apenas as âncoras citadas no fonte.
  • Os exemplos de JSON/XML são ilustrativos da forma definida pelas tags de struct; valores são fictícios.
  • "Banco de dados" foi interpretado como datastore de domínio persistente; os caches de arquivo (aliases 0600 com schemaVersion, DB do grype que não expira, o --advice-cache de aconselhamento) são tratados como caches reconstruíveis, não como modelo de dados.

Gaps

  • report.Version está hardcoded em "0.1.0" e diverge da versão do produto (v0.8.3); não ficou claro no código se isso é intencional (versionamento de contrato) ou defasagem. Documentado como observação.
  • Formato do crosswalk misto. aws.yaml ainda usa a forma legada (lista de topo, sem schemaVersion), enquanto azure/gcp/k8s.yaml usam a forma versionada (schemaVersion: 1 + controls). O loader aceita ambas; a migração de aws.yaml para a forma versionada é cosmética e não foi feita.
  • Diferente da v0.2.3, polaris e kube-score deixaram de ser placeholders: agora são adapters reais (internal/adapter/polaris.go, kubescore.go) e entradas do crosswalk/k8s.yaml. O gap anterior ("polaris sem adapter") está resolvido.
  • O conteúdo numérico exato de algumas seções do DESIGN.md (ex.: matriz de correlação completa) não foi transcrito; apenas o comportamento presente no código foi documentado.

Open Questions

  • A string report.Version deve passar a refletir a versão do produto (via ldflags do GoReleaser), ou é deliberadamente um versionamento de contrato independente?
  • O crosswalk/aws.yaml deve ser migrado para o formato versionado (schemaVersion: 1 + controls) por consistência com os demais, ou a forma legada permanece suportada indefinidamente?
  • Os controles de RBAC do kubescape ficam Unmapped por falta de segundo engine sobre manifestos estáticos. Se um scan com contexto de cluster ativo passar a fazer o polaris e o kubescape concordarem, o crosswalk/k8s.yaml deve ganhar esses pares?
  • O dump de []MergedFinding no campo findings do JSON inclui Members (cada um um Finding completo); isso é um contrato de saída estável e suportado, ou detalhe interno sujeito a mudança?