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(verDockerfile/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→stringpequeno; um banco relacional seria sobre-engenharia. Comentário literal eminternal/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 <target><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/<id>)"]
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()emcmd/quorum/scan.goretornafilepath.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.jsonno diretório corrente. - Override: flag
--cache <arquivo>. Passar--cache ""(string vazia) coloca o store em modo somente memória —cache.Open("")não lê nem grava em disco (verstore.goestore_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 EschemaVersioncasa com a versão atual Edatanã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 emaliases.json.tmpe fazos.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 primeiroPut. - Permissões: arquivo
0o600(dado por-usuário; sem motivo para ser world-readable — vale tanto para o.tmpquanto para o arquivo final após rename), diretório0o755. - Concorrência: seguro para uso concorrente dentro de um processo (
sync.RWMutex). Não há locking entre processos — doisquorum scansimultâ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--cachepara um caminho novo). - [ ] Em CI, cacheie este arquivo entre execuções para acelerar e reduzir chamadas ao OSV.dev (ex.:
actions/cachecom chave estável). - [ ] Use
--offlinepara 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
schemaVersiondiferente, nenhuma ação é necessária: o cache antigo é descartado e reconstruído transparentemente.
Observação sobre robustez do esquema: com o
schemaVersionembutido, 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/cachedo cache de aliases — logo, o mesmo envelope versionado ({schemaVersion, data},schemaVersion = 1), a mesma escrita por rename atômico, as mesmas permissões0o600e 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()emcmd/quorum/advisor.goretornafilepath.Join(os.UserCacheDir(), "quorum", "advice.json")(fallback.quorum-advice.jsonseos.UserCacheDir()falhar). Override com--advice-cache <arquivo>; um valor vazio coloca-o em modo somente memória. - Chave:
"advice:" + sha256(promptVersion | provider | model | fingerprint)(vercacheKeyeminternal/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 comtemperature=0. - Valor: o
model.Adviceserializado em JSON (texto da recomendação, provider, model, o rótulo"AI-generated, advisory only"e — sob--fix— umFixverificado). - 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:
.quorumignoreno 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
--baselineexplicitamente e o arquivo não existe → baseline vazio, scan prossegue (verscan.go:present == falsee flag não alterada). - Se o usuário passou
--baselineexplicitamente 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"]oucorrelationKeydo finding aceito. - [ ] Adicione ao
.quorumignorecom um comentário justificando e/ou um prazo de revisão. - [ ] Faça commit do
.quorumignoreno 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 dek8s.yaml).
5.2 Formato¶
YAML, aceito em duas formas (crosswalk.Load):
- Documento versionado (preferido, v0.8.3): um mapa de topo com
schemaVersion(constSchemaVersion = 1) econtrols(lista deControl). É o formato usado porazure.yaml,gcp.yamlek8s.yaml. - Lista pura (legada, ainda suportada): o arquivo inteiro é uma sequência YAML de
Controlno topo (sem cabeçalho de versão). É o formato ainda usado poraws.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):
- Lê 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 (
Loadretorna o erro;scan.goenvolve comoloading 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 ocanonicalControlno hub AVD (nuvem/IaC) ouC-####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 contadorcrosswalk=N rulesno 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 porcanonicalControl/ruleId/category/type. Anexamodel.Remediationemodel.DocRefa 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 viaquorum advise-index, e oscanescolhe 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ãoknowledge/owasp/corpus.yaml),--out(padrão: sobrescreve--corpusno local),--advice-endpoint(padrãohttp://localhost:11434/v1),--advice-embed-model(padrãonomic-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/knowledgetanto noDockerfile(:slim) quanto noDockerfile.full(:full) viaCOPY 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-indexreescreve 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:
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 viaENV GRYPE_DB_CACHE_DIR=/opt/grype/dbnoDockerfile.full. - Populado por:
RUN grype db update && grype db statusdurante o build da imagem. - Disponível somente na imagem
:full(que embute os scanners). A:slimnã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=truepara frescor máximo. - [ ] Verificar a idade do DB com
grype db statusdentro 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): incluipartialFingerprints["quorum/v1"]e oFingerprint = 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
Matchdo 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
--adviceestá ativo, cada finding pode carregar um blocoAdvice(recomendação, provider, model e o rótulo"AI-generated, advisory only") mais os campos determinísticosRemediation/References. São adições somente apresentação; sem--adviceo 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 comfilepath.Clean(colapsa segmentos./e../), cria o diretório pai se necessário (os.MkdirAll(dir, 0o755)) e escreve o arquivo com permissão0o600(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/v1empartialFingerprints. - 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) —1se o scanner rodou,0caso 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, porkind=remediation|references|recommendation.quorum_advice_provider{provider}— qual provider produziu advice (none|local|remote).quorum_advice_fix{stage}— fixes propostos vs. verificados, porstage=proposed|verified(a razãoverified/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ão0o644— 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(blocosboms: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=truedo BuildKit, a imagem:full/:slimrecebe atestação de SBOM viaactions/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
knowledgeatesta cada arquivo do pack embutido listado emknowledge.sha256(templates de remediação + o corpus OWASP com digest fixado + crosswalk), verificável comgh 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 deinternal/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 arquivoscrosswalk/{aws,azure,gcp,k8s}.yamle o packknowledge/(categories.yaml,owasp/corpus.yamle 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.jsone<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_AGEconfiguradas noDockerfile.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 eminternal/report; o conteúdo exato dosarif.gonã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.fulle 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-cachepodem 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
schemaVersionhabilita evolução de formato e coexistência com a lista legada, mas mudanças de campo do structControlcontinuam exigindo reescrever código + YAMLs. - Idade do Grype DB: congelado no build da imagem
:fulle 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 clearpara os caches de aliases e de advice, agora que o esquema já é versionado? - Qual a cadência oficial recomendada para rebuild/repull da imagem
:fulla 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
--crosswalkaponta 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?