Ir para o conteúdo

07 - Persistência e Artefatos

Versão: v0.8.3 · Revisão: 2026-07-04

Este documento descreve tudo o que o Quorum (quorum-sec-scan, v0.8.3) persiste em disco e lê de disco durante uma execução: formatos, localização, ciclo de vida, versionamento e estratégia de atualização/migração de cada artefato. O Quorum é uma ferramenta CLI/Docker de consensus security scanning que orquestra 12 scanners (SCA, misconfig/IaC, postura K8s e policy-as-code); não há banco de dados relacional, servidor de aplicação ou estado compartilhado de longa duração. A "persistência" do produto é deliberadamente minimalista e composta apenas por arquivos: um cache local de aliases (agora versionado por esquema), o baseline de supressão, os arquivos YAML de crosswalk (aws/azure/gcp/k8s, versionados), o banco de vulnerabilidades do Grype embutido na imagem, os arquivos de relatório de saída, um arquivo opcional de métricas Prometheus e — novidade da camada de advisory — um cache opcional de recomendações de IA mais o knowledge pack embutido (templates de remediação + um corpus OWASP com digest fixado).

Princípio de design relacionado: o cache e os artefatos auxiliares nunca podem ser fonte de falha de scan. Onde um artefato está ausente, ilegível ou corrompido, o Quorum degrada graciosamente para um comportamento seguro (ver internal/cache/store.go, internal/crosswalk/crosswalk.go, internal/filter/filter.go). A camada de advisory segue a mesma regra: é opt-in via --advice, somente apresentação, e sem ela a saída é byte-idêntica.

Documentos relacionados: Arquitetura · Modelo de Dados · Supply Chain · IA.


1. Banco de dados relacional — N/A

Status: N/A (não se aplica).

O Quorum não usa banco de dados relacional (PostgreSQL, MySQL, SQLite com SQL, etc.) e isso é uma decisão arquitetural, não uma lacuna.

Justificativa técnica:

  • Modelo de execução stateless / batch. O Quorum é invocado como um processo CLI de vida curta (quorum scan <target>), executa um pool de scanners em paralelo, correlaciona os resultados em memória e emite um relatório. Ao terminar o processo, não há estado a manter entre execuções — exceto o cache de aliases, que é puramente uma otimização (ver §2), e o cache opcional de recomendações de IA (§3.6), que também é uma otimização de reprodutibilidade/velocidade.
  • Sem multiusuário, sem concorrência entre processos. Não há contas de usuário, sessões, API REST ou serviço persistente. Não existe a necessidade de transações, controle de concorrência multi-writer ou consultas relacionais.
  • Portabilidade e zero-dependência. O binário é compilado com CGO_ENABLED=0 (ver Dockerfile/Dockerfile.full). Introduzir SQLite (CGO) ou um servidor de banco quebraria a premissa de "binário estático único" e a distribuição :slim (orquestrador puro, amd64+arm64).
  • O único estado que justificaria um KV store já é atendido por um arquivo JSON. O cache de aliases é um mapa string→string pequeno; um banco relacional seria sobre-engenharia. Comentário literal em internal/cache/store.go: "It gives the alias resolver idempotency and speed across CI re-scans without pulling in a CGO database."

Proposta futura (claramente separada — NÃO implementada)

Se, em uma evolução, o produto passar a oferecer um modo "servidor" (histórico de scans, dashboards, deduplicação cross-repo), aí sim um armazenamento estruturado seria pertinente. Mesmo nesse cenário, a recomendação seria começar por um KV/embedded store (ex.: BoltDB/Pebble) ou um datastore append-only, e não necessariamente um RDBMS. Isso está fora do escopo da v0.8.3.


2. Visão geral dos artefatos persistidos

flowchart LR
    subgraph Leitura["Entradas lidas de disco"]
        BL[".quorumignore<br/>baseline"]
        CW["crosswalk/*.yaml<br/>aws · azure · gcp · k8s<br/>(./crosswalk ou<br/>/opt/quorum/crosswalk)"]
        KN["knowledge pack<br/>templates + corpus OWASP<br/>(/opt/quorum/knowledge)<br/>(somente com --advice)"]
        POL["./policy/*.rego<br/>(conftest, opcional)"]
        GDB["grype DB<br/>/opt/grype/db"]
        CACHE_R["~/.cache/quorum/<br/>aliases.json (leitura)"]
        ADV_R["~/.cache/quorum/<br/>advice.json (leitura, --advice)"]
    end

    Q["quorum scan &lt;target&gt;<br/>(processo CLI)"]

    subgraph Escrita["Saídas escritas em disco"]
        REP["relatório<br/>SARIF / JSON / XML<br/>(-o ou stdout, 0600)"]
        MET["métricas Prometheus<br/>(--metrics, 0644)"]
        CACHE_W["~/.cache/quorum/<br/>aliases.json (escrita, 0600)"]
        ADV_W["~/.cache/quorum/<br/>advice.json (escrita, --advice, 0600)"]
    end

    BL --> Q
    CW --> Q
    KN --> Q
    POL --> Q
    GDB --> Q
    CACHE_R --> Q
    ADV_R --> Q
    Q --> REP
    Q --> MET
    Q --> CACHE_W
    Q --> ADV_W

Tabela-resumo de todos os artefatos:

Artefato Caminho padrão Formato Direção Escrito por Ciclo de vida
Cache de aliases ~/.cache/quorum/aliases.json (os.UserCacheDir()) JSON versionado ({schemaVersion, data}) leitura + escrita Quorum (internal/cache) persistente entre scans; regenerável
Cache de recomendações de IA ~/.cache/quorum/advice.json (os.UserCacheDir()) JSON versionado ({schemaVersion, data}) leitura + escrita (somente com --advice) Quorum (internal/advisor via internal/cache) persistente entre scans; regenerável; desligado por padrão
Baseline .quorumignore (cwd) texto, 1 entrada/linha leitura usuário versionado no repo do usuário
Crosswalk ./crosswalk → fallback /opt/quorum/crosswalk YAML versionado (schemaVersion + controls) ou lista legada leitura mantenedores / usuário bundled na imagem; versionado
Knowledge pack /opt/quorum/knowledge (bundled) Templates YAML + corpus OWASP com digest fixado leitura (somente com --advice) mantenedores bundled na imagem; versionado por release
Rego de policy ./policy/*.rego Rego (OPA) leitura usuário versionado no repo do usuário (via conftest)
Grype DB /opt/grype/db (GRYPE_DB_CACHE_DIR) DB do Grype/Syft (gerenciado pelo Grype) leitura build da imagem (Grype) congelado no build; atualizável
Relatório de saída -o <arquivo> ou stdout SARIF / JSON / XML escrita Quorum (internal/report) efêmero (artefato de CI)
Métricas --metrics <arquivo> Prometheus text-format escrita Quorum (internal/report) efêmero (scrape/textfile collector)

Os artefatos de supply chain do release (SBOM SPDX, atestações SLSA, assinaturas cosign) não são gerados por uma execução de scan; são produzidos no pipeline de build e cobertos na §10 e em Infraestrutura/Supply Chain. Desde a v0.8.3 o knowledge pack + crosswalk embutidos também recebem sua própria atestação SLSA (§10).


3. Cache de aliases — ~/.cache/quorum/aliases.json

3.1 Propósito

O resolvedor de aliases (internal/alias) normaliza qualquer identificador de vulnerabilidade para uma forma canônica (preferindo CVE), de modo que o GHSA-xxxx do Grype e o CVE-yyyy do Trivy para o mesmo bug correlacionem em vez de se dividirem. A cadeia de resolução (chainResolver.Canonical) é:

flowchart TD
    A["id de entrada"] --> B{"aliases locais<br/>do scanner contêm CVE?"}
    B -- sim --> R1["retorna CVE (sem tocar cache/rede)"]
    B -- não --> C{"hit no cache local<br/>aliases.json?"}
    C -- sim --> R2["retorna valor cacheado"]
    C -- não --> D{"--offline?"}
    D -- sim --> R3["preferCVE(aliases locais) → grava no cache"]
    D -- não --> E["consulta OSV.dev<br/>(/v1/vulns/&lt;id&gt;)"]
    E -- sucesso --> F["preferCVE(aliases do OSV) → grava no cache"]
    E -- falha de rede --> R3

O cache existe para dar idempotência e velocidade em re-scans de CI sem trazer um banco com CGO (comentário em internal/cache/store.go).

3.2 Formato

JSON puro, agora um envelope versionado (diskFormat em store.go) com dois campos: schemaVersion (inteiro, atualmente 1) e data (o mapa string → string de id de entrada → forma canônica). É indentado com 2 espaços (json.MarshalIndent(diskFormat{...}, "", " ")). Exemplo:

{
  "schemaVersion": 1,
  "data": {
    "GHSA-xxxx-yyyy-zzzz": "CVE-2024-12345",
    "GHSA-aaaa-bbbb-cccc": "CVE-2023-99999"
  }
}

Dentro de data, a chave é o id de entrada (não normalizado, como veio do scanner). O valor é o resultado de preferCVE(...) no momento da resolução (ver internal/alias/resolver.go, linha r.local.Put(id, canon)).

3.3 Localização

  • Padrão: defaultCachePath() em cmd/quorum/scan.go retorna filepath.Join(os.UserCacheDir(), "quorum", "aliases.json").
  • Linux: ~/.cache/quorum/aliases.json
  • macOS: ~/Library/Caches/quorum/aliases.json
  • Windows: %LocalAppData%\quorum\aliases.json
  • Fallback: se os.UserCacheDir() falhar, usa .quorum-cache.json no diretório corrente.
  • Override: flag --cache <arquivo>. Passar --cache "" (string vazia) coloca o store em modo somente memóriacache.Open("") não lê nem grava em disco (ver store.go e store_test.go).

3.4 Ciclo de vida e semântica de escrita

  • Abertura (cache.Open): lê o arquivo se existir; arquivo ausente ou ilegível resulta em cache vazio, não erro"a bad cache never breaks a scan". Além disso, o arquivo só é adotado quando parseia com sucesso E schemaVersion casa com a versão atual E data não é nulo; um arquivo legado/incompatível/corrompido degrada para cache vazio (rebuild único, sem migração nem leitura corrompida).
  • Escrita (cache.Put): ocorre a cada nova resolução que chega à camada 3 da cadeia. A escrita é atômica: serializa um snapshot dentro do envelope {schemaVersion, data}, grava em aliases.json.tmp e faz os.Rename(tmp, path). Falhas de flush são deliberadamente engolidas — o cache é otimização, nunca fonte de falha.
  • Criação de diretório: os.MkdirAll(filepath.Dir(path), 0o755) é feito preguiçosamente no primeiro Put.
  • Permissões: arquivo 0o600 (dado por-usuário; sem motivo para ser world-readable — vale tanto para o .tmp quanto para o arquivo final após rename), diretório 0o755.
  • Concorrência: seguro para uso concorrente dentro de um processo (sync.RWMutex). Não há locking entre processos — dois quorum scan simultâneos compartilhando o mesmo cache podem sobrescrever um ao outro; como a escrita é via rename atômico, o arquivo nunca fica corrompido, mas a última escrita vence (perda de algumas entradas, sem impacto de corretude pois cada entrada é regenerável).

3.5 Versionamento e migração

Aspecto Situação atual (v0.8.3)
Esquema versionado Sim. O envelope carrega schemaVersion (const schemaVersion = 1 em store.go).
Validação de esquema Adoção condicional: json.Unmarshal OK e schemaVersion igual ao atual e data != nil; caso contrário, cache vazio.
Migração Automática por descarte. Um arquivo com versão diferente é tratado como ausente e reconstruído uma vez — sem script de migração nem leitura mal-parseada.
TTL / expiração Não há. Entradas vivem indefinidamente.
Invalidação Manual (apagar o arquivo) ou automática ao bumpar schemaVersion.

Estratégia de atualização/regeneração (acionável):

  • [ ] Para forçar re-resolução via OSV, apague ~/.cache/quorum/aliases.json (ou aponte --cache para um caminho novo).
  • [ ] Em CI, cacheie este arquivo entre execuções para acelerar e reduzir chamadas ao OSV.dev (ex.: actions/cache com chave estável).
  • [ ] Use --offline para nunca consultar OSV; o cache + aliases locais do scanner passam a ser as únicas fontes.
  • [ ] Se um mapeamento de alias estiver errado (raro), apagar o arquivo é a "migração".
  • [ ] Ao migrar entre versões do Quorum com schemaVersion diferente, nenhuma ação é necessária: o cache antigo é descartado e reconstruído transparentemente.

Observação sobre robustez do esquema: com o schemaVersion embutido, uma mudança incompatível futura de formato passa a ser tratada de forma explícita (bump da constante → descarte controlado do cache antigo), fechando a lacuna que existia na v0.2.3. Ver Premissas.

3.6 Cache de recomendações de IA — ~/.cache/quorum/advice.json

Novidade da camada de advisory e só tocado quando --advice está ativo (sem ele, este arquivo nunca é lido nem escrito e a saída é byte-idêntica). A camada de advisory de IA (internal/advisor) anexa uma recomendação em linguagem natural — e, sob --fix=suggest, um patch verify-the-fix — a cada finding mesclado. Essas anexações são cacheadas em disco para que re-execuções de CI reaproveitem a recomendação de um finding em vez de reconsultar o modelo (reprodutibilidade + velocidade).

  • Store subjacente: o mesmo store de internal/cache do cache de aliases — logo, o mesmo envelope versionado ({schemaVersion, data}, schemaVersion = 1), a mesma escrita por rename atômico, as mesmas permissões 0o600 e a mesma degradação "a bad cache never breaks a scan". É um arquivo separado para nunca colidir com o mapa de aliases.
  • Localização: defaultAdviceCachePath() em cmd/quorum/advisor.go retorna filepath.Join(os.UserCacheDir(), "quorum", "advice.json") (fallback .quorum-advice.json se os.UserCacheDir() falhar). Override com --advice-cache <arquivo>; um valor vazio coloca-o em modo somente memória.
  • Chave: "advice:" + sha256(promptVersion | provider | model | fingerprint) (ver cacheKey em internal/advisor/advisor.go). Salgar por provider (local/remote) e model garante que uma resposta local e uma remota para o mesmo finding nunca colidam; salgar por promptVersion invalida recomendações defasadas quando o prompt muda. É isso que torna a recomendação reprodutível junto com temperature=0.
  • Valor: o model.Advice serializado em JSON (texto da recomendação, provider, model, o rótulo "AI-generated, advisory only" e — sob --fix — um Fix verificado).
  • Garantia: somente apresentação. O cache de advice nunca influencia correlationKey, fingerprint, confidence, severidade agregada ou o gate --fail-on — apenas guarda o que é anexado ao relatório. Se o modelo estiver inacessível, a execução segue sem advice de IA e o scan nunca falha.

Como o cache de advice guarda saída de modelo indexada pelo fingerprint do finding, trate-o como o relatório: pode refletir detalhe de finding. Por isso ele herda a permissão 0o600 (owner-only).


4. Baseline — .quorumignore

4.1 Propósito

O baseline é a lista de findings conhecidos/aceitos que devem ser suprimidos do relatório e do gating (--fail-on). Sem ele, --fail-on seria "ruído inutilizável" em CI (comentário em internal/filter/filter.go). Uma supressão sempre é logada (scan.go imprime filtered: N suppressed by baseline ...) — "a suppressed finding is still a finding".

4.2 Formato

Arquivo de texto, uma entrada por linha, onde cada entrada é um Fingerprint OU um CorrelationKey copiado de um relatório anterior. Regras de parsing (filter.LoadBaseline):

  • Linhas em branco são ignoradas.
  • Linhas iniciadas por # são comentários.
  • Comentário de fim de linha é permitido: entrada # nota.
  • A comparação é case-insensitive (entradas e findings são normalizados com strings.ToLower).

Exemplo:

# .quorumignore — findings aceitos para este repositório

# por fingerprint (sha256 do correlationKey)
3b1f...c0de   # CVE-2024-12345 em lib X — aceito até upgrade no Q3

# por correlationKey (mais legível, mais amplo)
VULN|CVE-2023-0001|pkg:npm/left-pad@1.0.0

Uma entrada por CorrelationKey suprime todos os findings que compartilham aquela chave; uma entrada por Fingerprint é mais específica. Ver Modelo de Dados para a definição de ambos.

4.3 Localização e ciclo de vida

  • Padrão: .quorumignore no diretório corrente (flag --baseline).
  • Ciclo de vida: é um artefato do usuário, versionado no repositório alvo (como .gitignore). O Quorum apenas o lê; nunca o escreve.
  • Ausência:
  • Se o usuário não passou --baseline explicitamente e o arquivo não existe → baseline vazio, scan prossegue (ver scan.go: present == false e flag não alterada).
  • Se o usuário passou --baseline explicitamente e o arquivo não existe → erro fatal (baseline file not found), exit code 2. Isso evita "supressão silenciosamente desligada" por caminho errado.

4.4 Versionamento e migração

Aspecto Situação atual
Esquema versionado Não aplicável — é uma lista de tokens opacos.
Migração Nenhuma necessária. Entradas que não casam com nenhum finding simplesmente não fazem efeito.
Manutenção Responsabilidade do usuário (revisar/limpar entradas obsoletas).

Checklist de uso (acionável):

  • [ ] Gere um relatório, copie o fingerprint/partialFingerprints["quorum/v1"] ou correlationKey do finding aceito.
  • [ ] Adicione ao .quorumignore com um comentário justificando e/ou um prazo de revisão.
  • [ ] Faça commit do .quorumignore no repositório alvo.
  • [ ] Revise periodicamente: como supressões são logadas, audite o stderr do CI.

5. Crosswalk — ./crosswalk/opt/quorum/crosswalk

5.1 Propósito

O crosswalk mapeia os rule ids próprios de cada scanner para um controle canônico compartilhado (hub AVD para IaC/nuvem, hub C-#### do Kubescape para postura K8s, com fallback de categoria semântica), de modo que misconfigs equivalentes vindos de engines diferentes correlacionem (internal/crosswalk/crosswalk.go, DESIGN §8). É o que ativa consenso além do SCA na v0.8.3. Princípio conservador: "false split > false merge" — só controles claramente equivalentes são agrupados, e os mapeamentos são derivados de saída real dos scanners e cross-checados semanticamente.

Cobertura por arquivo (quatro arquivos versionados no repo):

Arquivo Hub Engines mapeados Escopo (exemplos)
crosswalk/aws.yaml AVD-AWS trivy · checkov · kics · terrascan · regula S3, IAM, EBS, Security Groups, RDS, KMS, CloudTrail, VPC flow logs
crosswalk/azure.yaml AVD-AZU trivy · checkov · kics · terrascan · regula Storage Account, Key Vault
crosswalk/gcp.yaml AVD-GCP trivy · checkov · kics · terrascan · regula GCS bucket, firewall, Cloud SQL
crosswalk/k8s.yaml Kubescape C-#### kubescape · polaris · kube-score privilege-escalation, privileged, non-root, limites cpu/mem, probes, read-only-fs, linux-hardening, automount-SA, network-policy, host-network, host-PID/IPC, capabilities, secrets

Notas de correlação relevantes:

  • tfsec emite ids AVD nativamente, então suas findings auto-correlacionam com o Trivy sem precisar de entrada de crosswalk dedicada (comentário no Dockerfile.full).
  • RBAC segue single-engine (o RBAC do Kubescape exige contexto de cluster real e não é pareado a outro engine — documentado como decisão, não lacuna).
  • Checks agrupados do kube-score (ex.: container-resources = cpu+memory; pod-probes = liveness+readiness) são deixados intencionalmente unmapped para não sobre-mesclar um check composto em um único controle canônico (cabeçalho de k8s.yaml).

5.2 Formato

YAML, aceito em duas formas (crosswalk.Load):

  1. Documento versionado (preferido, v0.8.3): um mapa de topo com schemaVersion (const SchemaVersion = 1) e controls (lista de Control). É o formato usado por azure.yaml, gcp.yaml e k8s.yaml.
  2. Lista pura (legada, ainda suportada): o arquivo inteiro é uma sequência YAML de Control no topo (sem cabeçalho de versão). É o formato ainda usado por aws.yaml.

Load tenta a forma versionada primeiro (adota quando schemaVersion != 0 ou controls != nil) e, se não casar, faz fallback para a lista pura. Estrutura do Control (struct em crosswalk.go):

schemaVersion: 1
controls:
  - canonicalControl: AVD-AZU-0059      # controle canônico (hub)
    category: encryption                # categoria semântica (fallback)
    cwe: CWE-319                         # opcional
    title: "Storage account sem transferência segura (HTTPS)"
    ids:                                 # scanner -> [ruleIDs]
      trivy:     [AVD-AZU-0059]
      checkov:   [CKV_AZURE_3]
      kics:      ["12944ec4-1fa0-47be-8b17-42a034f937c2"]
      terrascan: [AC_AZURE_0373]

Indexação interna: ao carregar, cada par (scanner, ruleID) vira a chave "scanner|ruleid" (lowercased, trimmed) apontando para uma Resolution{Control, Category, CWE, Title} (ver key() e add() em crosswalk.go). Exemplos reais completos em crosswalk/aws.yaml e crosswalk/k8s.yaml.

5.3 Localização e resolução de diretório

A escolha do diretório é feita por resolveCrosswalkDir (cmd/quorum/scan.go):

flowchart TD
    A["--crosswalk"] --> B{"flag passada<br/>explicitamente?"}
    B -- sim --> R1["usa o valor literal"]
    B -- não --> C{"./crosswalk existe<br/>(é diretório)?"}
    C -- sim --> R2["usa ./crosswalk"]
    C -- não --> D{"/opt/quorum/crosswalk<br/>existe?"}
    D -- sim --> R3["fallback: /opt/quorum/crosswalk<br/>(crosswalk bundled na imagem)"]
    D -- não --> R4["usa ./crosswalk (vai carregar 0 regras)"]

O fallback existe para que docker run … scan . a partir de um workdir arbitrário ainda obtenha os mapeamentos embutidos, em vez de silenciosamente carregar 0 regras. As imagens :slim e :full copiam crosswalk para /opt/quorum/crosswalk (COPY crosswalk /opt/quorum/crosswalk nos dois Dockerfiles).

5.4 Carregamento e tolerância a falhas

crosswalk.Load(dir):

  • todos os arquivos *.yaml/*.yml (case-insensitive na extensão) no diretório e mescla todos os controles (independentemente de estarem na forma versionada ou na lista legada).
  • Subdiretórios são ignorados.
  • Diretório ausente NÃO é erro (os.IsNotExist → crosswalk vazio): a ferramenta roda sem crosswalk customizado, e cada lookup que erra mantém o finding isolado e marcado como unmapped (DESIGN §6, "never guess a match").
  • Erro de leitura de arquivo ou YAML inválido É erro fatal (Load retorna o erro; scan.go envolve como loading crosswalk: ..., exit 2). Diferente da ausência de diretório, um arquivo presente-mas-quebrado falha rápido.

O número de regras carregadas é logado: crosswalk=%d rules (%s) no stderr.

5.5 Versionamento, "tabela" e migração

O crosswalk é a tabela de mapeamento do produto (o análogo mais próximo de "schema/migrations" que o Quorum possui). Versionamento:

Aspecto Situação atual (v0.8.3)
Versionamento de conteúdo Acoplado à versão do Quorum: os YAMLs vivem no repo (crosswalk/) e são embutidos na imagem por versão (v0.8.3).
Campo de versão por arquivo Sim (novo). Const SchemaVersion = 1; forma versionada (schemaVersion + controls) suportada, com a lista legada ainda aceita para compatibilidade.
Migração de esquema Documentos versionados permitem evoluir o formato sem quebrar os arquivos legados; mudanças de campo do struct Control ainda exigem reescrever código + YAMLs (não há migração automática de dados).
Extensão pelo usuário Apontar --crosswalk <dir> para um diretório próprio, ou adicionar arquivos ao diretório bundled, substitui o default (não há merge entre default e custom além de Load mesclar arquivos do mesmo diretório).
Origem dos mapeamentos Derivados de saída real dos scanners e cross-checados semanticamente (cabeçalhos de aws.yaml/azure.yaml/gcp.yaml/k8s.yaml).

Checklist de manutenção/atualização (acionável):

  • [ ] Ao bumpar uma versão de scanner, re-rodar contra fixtures e revisar se rule ids mudaram (KICS usa UUIDs de query; podem mudar entre versões).
  • [ ] Adicionar novos controles como itens de controls:, mantendo o canonicalControl no hub AVD (nuvem/IaC) ou C-#### do Kubescape (K8s) quando existir.
  • [ ] Preferir a forma versionada (schemaVersion: 1 + controls:) para arquivos novos; a lista pura continua válida apenas por compatibilidade.
  • [ ] Para customização local: copiar o diretório bundled, editar, e passar --crosswalk <seu-dir>.
  • [ ] Validar com quorum scan ... e conferir o contador crosswalk=N rules no stderr.

6. Knowledge pack — /opt/quorum/knowledge (camada de advisory)

6.1 Propósito

O knowledge pack são os dados embutidos por trás da camada de advisory opt-in (--advice). É uma entrada somente leitura, consultada apenas quando o advice é solicitado, e alimenta as duas fases determinísticas de advisory que não precisam de modelo algum:

  • Fase 0 (templates + referências determinísticos, internal/enrich): templates de remediação curados e referências OWASP, casados por canonicalControl/ruleId/category/type. Anexa model.Remediation e model.DocRef a um finding sem modelo no loop.
  • Fase 2 (RAG-como-artefato, internal/rag): retrieval a partir de um corpus OWASP versionado e com digest fixado que fundamenta as recomendações. O retrieval é lexical por padrão (sem modelo); torna-se semântico (embeddings) quando o corpus é embutido via quorum advise-index, e o scan escolhe semântico automaticamente quando o corpus traz vetores.

Nenhuma das fases toca correlationKey/fingerprint/confidence/severidade agregada ou o gate --fail-on — como toda a camada de advisory, são somente apresentação.

6.2 Layout e formato

Tudo YAML, versionado no repo sob knowledge/ e embutido nas duas imagens em /opt/quorum/knowledge:

Arquivo Fase Conteúdo
knowledge/aws.yaml · azure.yaml · gcp.yaml · k8s.yaml · image.yaml 0 Templates de remediação curados + referências OWASP, indexados por controle/regra/tipo por domínio nuvem/K8s/imagem.
knowledge/categories.yaml 0 Fallbacks amplos por categoria semântica e por tipo de finding — anexa uma referência OWASP autoritativa (sem snippet) quando nenhuma entrada control-specific casou. schemaVersion: 1, entries:.
knowledge/owasp/corpus.yaml 2 O corpus RAG: passagens OWASP curtas (title + url + keywords + text) usadas para fundamentar advice. schemaVersion: 1, um digest: fixado e uma lista de chunks.

O corpus é fixado por digest: corpus.yaml carrega digest: "sha256:…" e o loader recomputa um hash de conteúdo sobre os chunks e recusa carregar em caso de divergência (guarda contra adulteração/truncamento). Embeddings estão intencionalmente ausentes por padrão — o retrieval cai no retriever lexical determinístico — e podem ser adicionados depois sem mudar o domínio de conteúdo fixado.

6.3 quorum advise-index — embutindo o corpus

O subcomando advise-index (cmd/quorum/advise_index.go) lê o corpus OWASP, embute cada chunk via um endpoint local compatível com OpenAI de embeddings (ex.: Ollama), e o reescreve com vetores embedding: por chunk. Os embeddings são excluídos do digest de conteúdo, então o pin é preservado. Uma vez presentes os vetores, scan --advice --advice-provider local passa automaticamente de retrieval lexical para semântico.

  • Flags: --corpus (padrão knowledge/owasp/corpus.yaml), --out (padrão: sobrescreve --corpus no local), --advice-endpoint (padrão http://localhost:11434/v1), --advice-embed-model (padrão nomic-embed-text).
  • É um passo de manutenção offline pontual, não parte de um scan.

6.4 Localização, ciclo de vida e tolerância a falhas

  • Localização: embutido em /opt/quorum/knowledge tanto no Dockerfile (:slim) quanto no Dockerfile.full (:full) via COPY knowledge /opt/quorum/knowledge. Fora de um container, usa-se o caminho empacotado sob o repo (knowledge/).
  • Ciclo de vida: somente leitura, versionado por release junto com o binário/imagem; um scan nunca o escreve (só advise-index reescreve o corpus, e apenas quando executado explicitamente).
  • Tolerância a falhas: o pack só é consultado sob --advice. Um arquivo de template ausente significa apenas nenhuma remediação determinística para aquele finding (o finding ainda é reportado). Uma divergência de digest do corpus faz o loader RAG recusar o corpus, então o grounding cai no que restar disponível — nunca uma falha de scan.

6.5 Versionamento e supply chain

O knowledge pack é acoplado à versão do Quorum. Desde a v0.8.3, o pack + crosswalk recebem uma atestação de build-provenance SLSA a cada release (o job knowledge em release.yml computa knowledge.sha256 sobre cada arquivo do pack e o atesta). Um consumidor pode verificar um arquivo embutido de forma independente, por exemplo:

gh attestation verify knowledge/owasp/corpus.yaml --repo Martinez1991/quorum-sec-scan

Ver §10 e Infraestrutura/Supply Chain para o panorama completo de proveniência de release, e IA para o enquadramento honesto da camada de advisory.


7. Grype DB — /opt/grype/db

7.1 Propósito

O banco de vulnerabilidades do Grype é necessário para que o scanner Grype funcione. Na imagem :full, ele é pré-cacheado no build para que o primeiro scan funcione offline e nunca atinja o erro "failed to load vulnerability db: database does not exist" (comentário no Dockerfile.full).

7.2 Formato e localização

  • Formato: banco do Grype/Syft, gerenciado internamente pelo próprio Grype (o Quorum não lê nem escreve este DB diretamente — apenas invoca o binário grype). É opaco para o Quorum.
  • Localização: /opt/grype/db, definido via ENV GRYPE_DB_CACHE_DIR=/opt/grype/db no Dockerfile.full.
  • Populado por: RUN grype db update && grype db status durante o build da imagem.
  • Disponível somente na imagem :full (que embute os scanners). A :slim não inclui scanners nem DB — espera scanners no PATH do runner.

7.3 Ciclo de vida, versionamento e atualização

Aspecto Situação atual (v0.8.3)
Estado no build DB congelado no momento do build da imagem :full.
Auto-update em runtime Desligado por padrão (ENV GRYPE_DB_AUTO_UPDATE=false).
Validação de idade Desligada (ENV GRYPE_DB_VALIDATE_AGE=false) — ver abaixo.
Atualização "on the fly" Definir GRYPE_DB_AUTO_UPDATE=true em runtime (requer rede) para o Grype atualizar o DB sozinho.
Atualização recomendada Reconstruir/repuxar a imagem :full para obter um DB mais recente.
Versionamento Acoplado à tag da imagem (GRYPE_VERSION pinado por digest no Dockerfile.full, atualmente v0.114.0) e à data do build.
Migração de schema do DB Gerenciada pelo Grype, não pelo Quorum.

Por que GRYPE_DB_VALIDATE_AGE=false é obrigatório aqui: por padrão o Grype recusa um DB mais velho que 5 dias (db.max-allowed-built-age) e passaria a falhar TODO scan assim que a imagem tivesse alguns dias de idade. Como o DB é assado no build (e a imagem pode ser usada por semanas em ambientes air-gapped), o Quorum desativa a validação de idade por design — aceita um DB potencialmente defasado e documenta a cadência de rebuild; frescor é uma escolha de supply chain, não um crash (comentário no Dockerfile.full).

flowchart LR
    BUILD["docker build :full<br/>grype db update"] --> FROZEN["DB congelado<br/>/opt/grype/db<br/>(data do build)"]
    FROZEN --> RUN{"runtime"}
    RUN -- "AUTO_UPDATE=false + VALIDATE_AGE=false (default)" --> USE["usa DB do build<br/>(offline OK, não expira)"]
    RUN -- "GRYPE_DB_AUTO_UPDATE=true (precisa rede)" --> UPD["grype atualiza DB on-the-fly"]
    NEWIMG["rebuild / repull da imagem :full"] -.refresh recomendado.-> FROZEN

Implicação operacional: o DB envelhece junto com a imagem, mas não expira (não há mais hard-fail por idade). Um DB desatualizado pode perder CVEs recentes — isso reforça o aviso do produto "0 findings is not proof of safety". Mantenha a imagem :full atualizada em pipelines de longa duração.

Checklist (acionável):

  • [ ] Para ambientes air-gapped: confiar no DB do build e repuxar a imagem em cadência regular (ex.: semanal).
  • [ ] Para ambientes com rede: considerar GRYPE_DB_AUTO_UPDATE=true para frescor máximo.
  • [ ] Verificar a idade do DB com grype db status dentro do container.

8. Relatórios de saída — SARIF / JSON / XML

8.1 Propósito e formato

Saída final do scan, serializada por internal/report (report.Write, seletor Format em report.go):

  • SARIF (primário, --format sarif, default): inclui partialFingerprints["quorum/v1"] e o Fingerprint = sha256(correlationKey) para integração com plataformas de code scanning.
  • JSON (--format json): modelo canônico serializado.
  • XML (--format xml).

Formato inválido → erro de uso (exit 2). Ver report.ParseFormat (want sarif|json|xml).

Redaction de segredos: findings de scanners que carregam trechos casados de segredo (ex.: o Match do Trivy) são redigidos antes de entrar no relatório, para que o artefato de saída não vaze o próprio segredo detectado. Ver Modelo de Dados.

Advice de IA no relatório: quando --advice está ativo, cada finding pode carregar um bloco Advice (recomendação, provider, model e o rótulo "AI-generated, advisory only") mais os campos determinísticos Remediation/References. São adições somente apresentação; sem --advice o relatório é byte-idêntico ao núcleo determinístico.

8.2 Localização e ciclo de vida

emit() em cmd/quorum/scan.go:

  • Sem -o/--output: escreve em stdout (cmd.OutOrStdout()).
  • Com -o <arquivo>: o caminho é normalizado com filepath.Clean (colapsa segmentos ./ e ../), cria o diretório pai se necessário (os.MkdirAll(dir, 0o755)) e escreve o arquivo com permissão 0o600 (owner-only — o relatório pode carregar detalhe sensível de finding e não deve ser world-readable por padrão), sobrescrevendo se existir.
  • Ciclo de vida: efêmero. É um artefato de CI/saída; o Quorum não mantém histórico nem reabre relatórios. Persistência/retenção é responsabilidade do pipeline (ex.: upload de SARIF para GitHub Code Scanning, artefato de build).

8.3 Versionamento

  • O esquema SARIF segue o padrão SARIF; o namespace de fingerprint próprio é versionado explicitamente como quorum/v1 em partialFingerprints.
  • Não há "migração" — cada execução produz um relatório novo e completo.

9. Métricas — --metrics <arquivo> (Prometheus)

9.1 Propósito e formato

--metrics <arquivo> escreve métricas em formato texto Prometheus (report.WriteMetrics, internal/report/metrics.go), adequadas a um textfile collector do node_exporter ou a um Pushgateway — telemetria exportável para uma CLI que não tem processo de longa duração a ser scraped. Séries emitidas incluem, entre outras:

  • quorum_scan_duration_seconds (gauge) — tempo total de wall-clock do scan.
  • quorum_scanner_up{scanner,status} (gauge) — 1 se o scanner rodou, 0 caso contrário (skipped/unavailable/error/timeout).

Quando --advice está ativo, a camada de advisory adiciona suas próprias séries (ainda contagens não-sensíveis):

  • quorum_advice_enriched{kind} — enriquecimentos determinísticos anexados, por kind = remediation | references | recommendation.
  • quorum_advice_provider{provider} — qual provider produziu advice (none | local | remote).
  • quorum_advice_fix{stage} — fixes propostos vs. verificados, por stage = proposed | verified (a razão verified/proposed é a taxa de verify-the-fix).

São contagens não-sensíveis, feitas para serem coletadas.

9.2 Localização e ciclo de vida

writeMetricsFile() em cmd/quorum/scan.go:

  • O caminho é normalizado com filepath.Clean, o diretório pai é criado (os.MkdirAll(dir, 0o755)) e o arquivo é escrito com permissão 0o644 — diferentemente do relatório (0600), pois métricas são contagens não-sensíveis destinadas a scraping.
  • Ciclo de vida: efêmero, sobrescrito a cada execução; retenção/coleta é responsabilidade do pipeline ou do coletor.

Complementarmente, --log-format text|json controla o formato dos logs de progresso no stderr (não gera arquivo). Caps anti-DoS relevantes a I/O: QUORUM_MAX_TARGET_BYTES (padrão 20 GiB, limite de tamanho do target) e QUORUM_MAX_OUTPUT_BYTES (padrão 512 MiB, teto de output por scanner). Passthrough por scanner via QUORUM_<SCANNER>_ARGS (ex.: QUORUM_CHECKOV_ARGS=--bc-api-key ...).


10. SBOMs e atestações de release — artefatos de supply chain

Status: gerados no pipeline de build, não em uma execução de scan.

Diferente dos artefatos das seções anteriores (lidos/escritos por quorum scan), estes são produzidos pelo release (.github/workflows/release.yml, .goreleaser.yaml, Dockerfile.full) e vivem no GHCR / GitHub Releases, não no disco de runtime:

  • SBOM SPDX por binário/arquivo: o GoReleaser gera um SBOM SPDX por archive via syft (bloco sboms: em .goreleaser.yaml), de modo que cada release publica um bill of materials legível por máquina dos binários.
  • SBOM SPDX atestado para a imagem: além do sbom=true do BuildKit, a imagem :full/:slim recebe atestação de SBOM via actions/attest-sbom.
  • Procedência SLSA: atestação de build-provenance (actions/attest-build-provenance) para a imagem e por binário.
  • Proveniência do knowledge pack + crosswalk (novo na v0.8.3): o job knowledge atesta cada arquivo do pack embutido listado em knowledge.sha256 (templates de remediação + o corpus OWASP com digest fixado + crosswalk), verificável com gh attestation verify knowledge/owasp/corpus.yaml.
  • Assinatura cosign keyless: assinatura sobre o arquivo de checksums (cobre todos os artefatos por hash) e verificação keyless com retry no pipeline.
  • THIRD_PARTY_NOTICES.md: avisos de terceiros publicados junto.

Ciclo de vida: imutáveis por release, versionados pela tag semver; a tag móvel v0 é avançada automaticamente a cada release (tag-major.yml). Detalhes completos em Infraestrutura/Supply Chain.


11. Resumo de tolerância a falhas por artefato

Artefato Ausente Ilegível / corrompido Estratégia
Cache de aliases cache vazio, scan prossegue cache vazio, scan prossegue (também se schemaVersion divergir) sempre degrada; nunca falha
Cache de advice de IA cache vazio, scan prossegue (só com --advice) cache vazio, scan prossegue (mesmo store do cache de aliases) opt-in; somente apresentação; nunca falha
Baseline (.quorumignore) vazio se default; erro se passado explicitamente erro de leitura propagado (exceto not-exist) falha explícita só quando o usuário pediu o arquivo
Crosswalk (dir) crosswalk vazio (0 regras), findings ficam unmapped erro fatal se arquivo presente-mas-inválido ausência tolerada; conteúdo quebrado falha rápido
Knowledge pack sem remediação determinística para aquele finding (só com --advice) divergência de digest do corpus → loader RAG recusa o corpus, grounding degrada opt-in; nunca falha de scan
Grype DB erro do Grype (mas :full sempre tem) gerenciado pelo Grype; não expira (VALIDATE_AGE=false) pré-cacheado no build
Relatório de saída N/A (é escrita) N/A filepath.Clean; cria diretório pai; sobrescreve (0600)
Métricas N/A (é escrita) N/A filepath.Clean; cria diretório pai; sobrescreve (0644)

Premissas

  • A descrição reflete a base de código na v0.8.3 (branch main), conforme leitura de internal/cache/store.go, internal/alias/resolver.go, internal/alias/osv.go, internal/crosswalk/crosswalk.go, internal/filter/filter.go, internal/advisor/advisor.go, cmd/quorum/advisor.go, cmd/quorum/advise_index.go, cmd/quorum/scan.go, internal/report/report.go, internal/report/metrics.go, Dockerfile, Dockerfile.full, .goreleaser.yaml, .github/workflows/release.yml, os quatro arquivos crosswalk/{aws,azure,gcp,k8s}.yaml e o pack knowledge/ (categories.yaml, owasp/corpus.yaml e os templates de nuvem/K8s/imagem).
  • Caminhos por SO do cache derivam do contrato de os.UserCacheDir() da stdlib do Go; os exemplos por SO (Linux/macOS/Windows) seguem a documentação dessa função, não uma string hardcoded no código (o código só compõe <UserCacheDir>/quorum/aliases.json e <UserCacheDir>/quorum/advice.json).
  • O cache de advice de IA compartilha o store de internal/cache, então seu envelope, escrita atômica e propriedades de degradação são os mesmos do cache de aliases; é um arquivo separado (advice.json) e só é exercitado sob --advice.
  • O formato interno do Grype DB é tratado como opaco pelo Quorum; afirmações sobre ele baseiam-se no contrato do Grype e nas variáveis GRYPE_DB_CACHE_DIR/GRYPE_DB_AUTO_UPDATE/GRYPE_DB_VALIDATE_AGE configuradas no Dockerfile.full, não em leitura do binário do DB.
  • "Relatório efêmero" e "métricas efêmeras" assumem o uso típico em CI; o Quorum não impõe retenção — qualquer persistência é externa ao produto.
  • Os detalhes de SARIF (partialFingerprints["quorum/v1"], Fingerprint = sha256(correlationKey)) baseiam-se na especificação do produto e no roteamento em internal/report; o conteúdo exato do sarif.go não foi citado linha a linha aqui (ver Modelo de Dados).
  • Os artefatos de supply chain (SBOM/SLSA/cosign) são descritos a partir de .goreleaser.yaml, Dockerfile.full e dos workflows de release; o detalhamento operacional vive em Infraestrutura.

Gaps conhecidos

  • Cache sem TTL/invalidação automática: entradas (tanto no cache de aliases quanto no de advice) vivem indefinidamente; só a remoção manual do arquivo (ou um bump de schemaVersion) as renova.
  • Sem locking entre processos nos caches: execuções concorrentes que compartilham o mesmo --cache/--advice-cache podem perder entradas (sem corrupção, graças ao rename atômico).
  • Crosswalk custom substitui, não mescla: apontar --crosswalk <dir> para outro diretório troca o default por completo; não há merge entre o diretório bundled e o custom.
  • Migração de dados do crosswalk ainda manual: o schemaVersion habilita evolução de formato e coexistência com a lista legada, mas mudanças de campo do struct Control continuam exigindo reescrever código + YAMLs.
  • Idade do Grype DB: congelado no build da imagem :full e sem expiração por idade (VALIDATE_AGE=false); sem rebuild, pode perder CVEs recentes silenciosamente.

Perguntas em aberto

  • Vale introduzir um TTL configurável ou um comando quorum cache clear para os caches de aliases e de advice, agora que o esquema já é versionado?
  • Qual a cadência oficial recomendada para rebuild/repull da imagem :full a fim de manter o Grype DB fresco em pipelines de longa duração, dado que a validação de idade está desligada?
  • O crosswalk customizado deveria mesclar com o bundled (em vez de substituir) quando --crosswalk aponta para outro diretório?
  • Faz sentido migrar aws.yaml (ainda na forma de lista legada) para a forma versionada (schemaVersion + controls), padronizando os quatro arquivos?