Requisitos Funcionais¶
Este documento especifica os Requisitos Funcionais (RF) do Quorum (quorum-sec-scan), versão v0.8.3 (revisão 2026-07-04). O Quorum é uma ferramenta CLI/Docker de consensus security scanning: ela orquestra um conjunto de 12 scanners open-source (trivy, grype, checkov, kics, dockle, kubescape, polaris, kube-score, terrascan, tfsec, regula, conftest), normaliza cada achado para um modelo canônico (model.Finding), resolve aliases de vulnerabilidade, correlaciona achados equivalentes por uma chave determinística (correlationKey), pontua a confiança de consenso (confidence) e emite um relatório unificado (SARIF/JSON/XML) com gating por código de saída para CI/CD.
Cada RF abaixo é derivado diretamente do código-fonte (cmd/quorum/*.go e internal/*) e descrito com ID, Nome, Descrição, Fluxo, Entradas, Saídas, Regras de negócio, Prioridade e Dependências. Os RFs cobrem a superfície real do produto como está (as-is); capacidades que não existem (frontend web, banco relacional, API REST, autenticação embutida) são marcadas como N/A na seção Itens fora de escopo (N/A).
Princípio de projeto que permeia todos os RFs: "false split > false merge" e "0 achados não é prova de segurança". O sistema prefere isolar um achado a mesclá-lo incorretamente e nunca trata "0 vulnerabilidades" como prova de segurança — o status por scanner é sempre reportado.
O que mudou desde a v0.2.3 (as-is v0.8.3): o conjunto cresceu de 6 para 12 scanners; o consenso passou a valer também para MISCONFIG/K8S_POSTURE/IaC (não só SCA), via um crosswalk derivado da saída real (
crosswalk/{aws,azure,gcp,k8s}.yaml); novos flags--metricse--log-format; passthrough de argumentos por scanner viaQUORUM_<SCANNER>_ARGS; caps de DoS para a saída dos scanners e o tamanho do alvo; validação dotargetcontra argument injection; e hardening de I/O (relatório com0600+filepath.Clean).O que é novo desde a v0.7.4 (as-is v0.8.3): uma CAMADA CONSULTIVA opt-in (
--advice). Ela é apenas de apresentação — nunca toca emcorrelationKey,fingerprint,confidence, severidade agregada ou no gate--fail-on; sem--advicea saída é byte-a-byte idêntica. O núcleo determinístico continua sem IA. A camada tem quatro fases: Fase 0 (templates determinísticos de remediação + referências OWASP a partir de um knowledge pack, RF-028), Fase 2 (referências OWASP determinísticas via RAG a partir de um corpus fixado por digest + o subcomandoadvise-index, RF-029), Fase 1 (recomendação opt-in de LLM local, RF-030) e o modo de correção sugerida com re-scan verify-the-fix (RF-031). O provedor remoto (Fase 3) é egress-gated. Novos flags de CLI:--advice,--advice-provider,--advice-endpoint,--advice-model,--advice-embed-model,--advice-cache,--advice-max,--advice-allow-egress,--knowledge,--fix.
Índice de requisitos¶
| ID | Nome | Prioridade | Componente principal |
|---|---|---|---|
| RF-001 | Comando scan para um alvo |
Obrigatório | cmd/quorum/scan.go |
| RF-002 | Inferência e seleção do tipo de alvo | Obrigatório | cmd/quorum/scan.go |
| RF-003 | Listar scanners registrados (list-scanners) |
Obrigatório | cmd/quorum/root.go |
| RF-004 | Seleção de scanners | Obrigatório | orchestrator, adapter |
| RF-005 | Execução paralela (fan-out) com timeout por scanner | Obrigatório | orchestrator |
| RF-006 | Sondagem de versão e disponibilidade do scanner | Obrigatório | orchestrator, adapter |
| RF-007 | Status por scanner (transparência de execução) | Obrigatório | orchestrator, report |
| RF-008 | Normalização canônica de achados | Obrigatório | adapter, model, severity |
| RF-009 | Resolução de aliases de vulnerabilidade | Obrigatório | alias, cache |
| RF-010 | Modo offline (OSV desabilitado) | Obrigatório | cmd/quorum/scan.go, alias |
| RF-011 | Cache de aliases | Recomendado | cache |
| RF-012 | Crosswalk regra → controle canônico | Obrigatório | crosswalk, correlate |
| RF-013 | Fallback automático do crosswalk empacotado | Recomendado | cmd/quorum/scan.go |
| RF-014 | Correlação por correlationKey e fingerprint |
Obrigatório | correlate |
| RF-015 | Pontuação e agregação de consenso | Obrigatório | consensus |
| RF-016 | Emissão de relatório SARIF/JSON/XML | Obrigatório | report |
| RF-017 | Baseline de supressão (.quorumignore) |
Obrigatório | filter |
| RF-018 | Filtro de severidade mínima (--min-severity) |
Obrigatório | filter, severity |
| RF-019 | Gate de falha (--fail-on) e códigos de saída |
Obrigatório | cmd/quorum/scan.go, severity |
| RF-020 | Saída para arquivo ou stdout | Obrigatório | cmd/quorum/scan.go |
| RF-021 | Resumo no console e logs de progresso | Recomendado | cmd/quorum/scan.go |
| RF-022 | Versão da ferramenta | Recomendado | cmd/quorum/root.go |
| RF-023 | Exportação de métricas Prometheus (--metrics) |
Recomendado | cmd/quorum/scan.go, report |
| RF-024 | Formato dos logs de progresso (--log-format) |
Recomendado | cmd/quorum/scan.go |
| RF-025 | Passthrough de argumentos por scanner (QUORUM_<SCANNER>_ARGS) |
Recomendado | internal/adapter |
| RF-026 | Caps de DoS: saída e tamanho do alvo | Obrigatório | internal/adapter, cmd/quorum/scan.go |
| RF-027 | Validação do alvo (injeção de argumentos) | Obrigatório | cmd/quorum/scan.go |
| RF-028 | Enriquecimento consultivo (--advice, knowledge pack) |
Recomendado | internal/enrich, cmd/quorum/scan.go |
| RF-029 | Referências OWASP via RAG e advise-index |
Recomendado | internal/rag, cmd/quorum/advise_index.go |
| RF-030 | Recomendações de IA (--advice-provider) |
Opcional | internal/advisor |
| RF-031 | Correção sugerida (--fix) com verify-the-fix |
Opcional | internal/advisor |
Visão geral do pipeline¶
O pipeline executado pelo comando scan é linear e determinístico após a fase de fan-out. A camada consultiva (RF-028…RF-031) é um estágio de apresentação opt-in acoplado depois do consenso e dos filtros, antes de o relatório ser escrito; ela nunca realimenta o gate.
flowchart TD
A["scan target (flags)"] --> A0["RF-027: valida a ref do alvo<br/>(rejeita prefixo '-')"]
A0 --> B["RF-002: resolve o tipo do alvo<br/>image | repo | k8s"]
B --> B0["RF-026: cap de tamanho do alvo<br/>(QUORUM_MAX_TARGET_BYTES)"]
B0 --> C["RF-004: seleciona adapters<br/>(todos ou --scanners)"]
C --> D["RF-005: fan-out paralelo<br/>(goroutines + timeout)"]
D --> E["RF-006: sondagem de versão<br/>(60s, distingue OOM/timeout/ausente)"]
E --> F["RF-008/RF-025/RF-026: normaliza<br/>saída → model.Finding (extraArgs, cap de saída)"]
F --> G["RF-009/RF-012: enriquece<br/>(aliases VULN + crosswalk MISCONFIG/K8S/IaC)"]
G --> H["RF-014: correlationKey + fingerprint"]
H --> I["RF-015: consenso<br/>(merge + confidence + severidade agregada)"]
I --> J["RF-017/RF-018: filtros<br/>(baseline + min-severity)"]
J --> J2["RF-028/029/030/031: camada consultiva (--advice, opt-in)<br/>remediação + refs OWASP + recomendação de IA + fix"]
J2 --> K["RF-016/RF-020: relatório<br/>SARIF | JSON | XML → arquivo/stdout"]
K --> L["RF-007/RF-021/RF-024: status por scanner + resumo (text/json)"]
L --> L2["RF-023: métricas Prometheus (--metrics)"]
L2 --> M["RF-019: gate fail-on → código de saída"]
Referência de pacotes: cmd/quorum (CLI cobra) → internal/orchestrator → internal/adapter (12 scanners) → internal/{alias,cache,crosswalk} (enriquecimento) → internal/correlate → internal/consensus → internal/filter → internal/{enrich,rag,advisor} (camada consultiva opt-in) → internal/report.
RF-001 — Comando scan para um alvo¶
| Campo | Conteúdo |
|---|---|
| ID | RF-001 |
| Nome | Comando scan para um alvo |
| Descrição | O usuário deve conseguir escanear um único alvo (imagem de container, repositório/diretório IaC ou manifests k8s) executando o conjunto de scanners e produzindo um relatório de consenso. É o comando central da ferramenta. |
| Prioridade | Obrigatório |
Fluxo
sequenceDiagram
actor U as Usuário/CI
participant C as quorum scan
participant O as orchestrator
participant R as report
U->>C: quorum scan <target> [flags]
C->>C: valida ref do alvo, --log-format, --fail-on, --min-severity, --format, --baseline, --advice-provider, --fix
C->>C: cap de tamanho do alvo (RF-026)
C->>O: orchestrator.Run(ctx, target, options)
O-->>C: Result (Runs, Findings, Merged)
C->>C: filter.Apply (baseline + min-severity)
C->>C: camada consultiva (--advice, opt-in) → remediação/refs/IA (RF-028…RF-031)
C->>R: report.Write (SARIF/JSON/XML)
R-->>U: relatório (arquivo ou stdout)
C->>C: --metrics (opcional) → arquivo Prometheus
C->>U: resumo em stderr + código de saída
Entradas
| Entrada | Origem | Obrigatória |
|---|---|---|
<target> (1 argumento posicional) |
cobra.ExactArgs(1) |
Sim |
Flags --type, --scanners, --format/-f, --output/-o, --fail-on, --min-severity, --baseline, --crosswalk, --knowledge, --cache, --metrics, --log-format, --timeout, --offline, --quiet/-q |
CLI | Não (têm defaults) |
Flags consultivas --advice, --advice-provider, --advice-endpoint, --advice-model, --advice-embed-model, --advice-cache, --advice-max, --advice-allow-egress, --fix |
CLI | Não (desligadas por padrão) |
Variáveis de ambiente QUORUM_<SCANNER>_ARGS, QUORUM_MAX_OUTPUT_BYTES, QUORUM_MAX_TARGET_BYTES, QUORUM_ADVICE_API_KEY |
Ambiente | Não |
Saídas: relatório no formato escolhido (stdout ou arquivo), resumo de execução em stderr, arquivo de métricas opcional, código de saída (0/1/2).
Regras de negócio
- cobra.ExactArgs(1): exatamente um alvo é obrigatório; 0 ou 2+ argumentos → erro de uso (exit 2).
- O target é validado contra argument injection antes de qualquer outra etapa (RF-027).
- Flags inválidos (por exemplo, --fail-on fora de critical|high|medium|low, --format desconhecido, --log-format ≠ text|json, --advice-provider ≠ none|local|remote, --fix ≠ off|suggest) abortam antes de qualquer scanner rodar.
- O comando nunca falha por causa de um cache/aliases/crosswalk corrompido — eles degradam graciosamente (ver RF-009, RF-011, RF-012). Da mesma forma, a camada consultiva degrada graciosamente e nunca faz a varredura falhar (RF-028…RF-031).
Dependências: RF-002, RF-004, RF-005, RF-008, RF-014, RF-015, RF-016, RF-017, RF-018, RF-019, RF-026, RF-027; opcionalmente RF-028…RF-031.
RF-002 — Inferência e seleção do tipo de alvo¶
| Campo | Conteúdo |
|---|---|
| ID | RF-002 |
| Nome | Inferência e seleção do tipo de alvo |
| Descrição | O tipo do alvo (image, repo, k8s) pode ser fornecido via --type ou inferido automaticamente. O tipo seleciona quais adapters são aplicáveis e como cada scanner é invocado. |
| Prioridade | Obrigatório |
Fluxo / Regras de mapeamento (resolveTargetType em scan.go)
Valor de --type |
Resultado |
|---|---|
image |
TargetImage |
repo / fs / dir |
TargetRepo |
k8s / kubernetes / manifests |
TargetK8s |
| (vazio) + caminho existe em disco | TargetRepo (inferido) |
| (vazio) + caminho não existe | TargetImage (inferido — assume uma ref de imagem) |
| qualquer outro valor | erro invalid --type (exit 2) |
Entradas: --type (string, case-insensitive), <target> (usado em os.Stat para inferência).
Saídas: adapter.TargetType resolvido, propagado para adapter.Target{Type, Ref}.
Regras de negócio
- A inferência usa os.Stat(ref): existe em disco → repo; caso contrário → image.
- O tipo é case-insensitive (strings.ToLower).
- O tipo determina o Supports(target) de cada adapter (RF-004) e os argumentos de CLI de cada scanner (RF-008).
Dependências: RF-001, RF-027 (validação da ref). Consumido por RF-004, RF-008 e RF-026 (o cap de tamanho aplica-se apenas a repo/k8s).
RF-003 — Listar scanners registrados (list-scanners)¶
| Campo | Conteúdo |
|---|---|
| ID | RF-003 |
| Nome | Listar scanners registrados (list-scanners) |
| Descrição | O usuário deve conseguir listar todo adapter de scanner registrado e os tipos de achado que cada um cobre (capacidades). |
| Prioridade | Obrigatório |
Fluxo (newListScannersCmd em root.go): obtém adapter.Names(), ordena-os e, para cada nome, imprime name + a lista de Capability.Type.
Entradas: nenhuma (sem argumentos ou flags próprios).
Saídas (stdout, uma linha por scanner, formato %-12s %v, ordenado alfabeticamente), os 12 scanners registrados na v0.8.3:
| Scanner | Tipos cobertos (Capabilities) |
|---|---|
checkov |
[MISCONFIG] |
conftest |
[MISCONFIG] |
dockle |
[IMG_HARDENING] |
grype |
[VULN] |
kics |
[MISCONFIG] |
kube-score |
[K8S_POSTURE] |
kubescape |
[K8S_POSTURE] |
polaris |
[K8S_POSTURE] |
regula |
[MISCONFIG] |
terrascan |
[MISCONFIG] |
tfsec |
[MISCONFIG] |
trivy |
[VULN MISCONFIG SECRET] |
Regras de negócio
- A lista vem do registry populado em tempo de compilação pelo init() de cada adapter (adapter.Register).
- A saída é ordenada alfabeticamente por nome (sort.Strings); note que kube-score precede kubescape (o hífen ordena antes de s).
- Não verifica se o binário do scanner está instalado — lista capacidades declaradas, não disponibilidade em tempo de execução (isso é tarefa do RF-006 durante o scan).
Dependências: registro de adapters (adapter.Register, adapter.Names, adapter.Get, Capabilities).
RF-004 — Seleção de scanners¶
| Campo | Conteúdo |
|---|---|
| ID | RF-004 |
| Nome | Seleção de scanners |
| Descrição | Por padrão, todos os adapters que suportam o alvo são executados; o usuário pode restringir a um subconjunto via --scanners (lista separada por vírgulas). Nomes desconhecidos geram aviso, não fazem o scan falhar. |
| Prioridade | Obrigatório |
Fluxo (splitScanners em scan.go + selectAdapters em orchestrator.go)
flowchart TD
A["--scanners vazio?"] -->|Sim| B["adapter.All() ordenado por nome"]
A -->|Não| C["para cada nome (lowercase, trim)"]
C --> D{"adapter.Get(name) existe?"}
D -->|Sim| E["adiciona à seleção"]
D -->|Não| F["acrescenta a 'unknown' → aviso"]
B --> G["filtra por Supports(target) em runOne"]
E --> G
Entradas: --scanners (string CSV, ex. trivy,grype,tfsec), target.
Saídas: conjunto de adapters a executar; lista de nomes desconhecidos (para aviso).
Regras de negócio
- --scanners vazio ⇒ todos os 12 adapters registrados (adapter.All()), ordenados por nome.
- Os nomes são normalizados para lowercase e sofrem trim; entradas vazias são descartadas.
- Um nome inexistente ⇒ vai para unknown e emite o aviso unknown scanner %q ignored (known: ...) — não aborta o scan.
- Mesmo quando selecionado, um scanner que não suporta o tipo de alvo é marcado como skipped (ver RF-006).
Dependências: RF-002 (tipo do alvo), registro de adapters. Consumido por RF-005.
RF-005 — Execução paralela (fan-out) com timeout por scanner¶
| Campo | Conteúdo |
|---|---|
| ID | RF-005 |
| Nome | Execução paralela (fan-out) com timeout por scanner |
| Descrição | Os scanners selecionados rodam concorrentemente (uma goroutine por scanner), cada um com um timeout individual configurável. Os resultados são coletados e agregados ao final. |
| Prioridade | Obrigatório |
Fluxo (orchestrator.Run + runOne): para cada adapter selecionado é criada uma goroutine; um sync.WaitGroup espera por todas; um sync.Mutex protege a coleta de Runs e findings. Após wg.Wait(), os runs são ordenados por nome.
Entradas: adapters selecionados (RF-004), --timeout (mapeado para Options.PerScannerTime, default 5m), context.Background().
Saídas: []ScannerRun (status/duração/erro por scanner) + []model.Finding agregado.
Regras de negócio
- Cada scanner roda com context.WithTimeout(ctx, PerScannerTime) quando PerScannerTime > 0. Estouro de deadline ⇒ status timeout (RF-007).
- PerScannerTime == 0 ⇒ sem timeout extra (apenas o do context raiz).
- A coleta concorrente é protegida por mutex; a ordem final de Runs é determinística (ordenada por nome) independentemente da ordem de término.
- runCmd trata um código de saída não-zero com saída em stdout como sucesso (vários scanners retornam não-zero quando encontram problemas); saída não-zero sem stdout vira erro com stderr anexado. A saída bufferizada é limitada por um cap (RF-026).
Dependências: RF-004, RF-006. Alimenta RF-008.
RF-006 — Sondagem de versão e disponibilidade do scanner¶
| Campo | Conteúdo |
|---|---|
| ID | RF-006 |
| Nome | Sondagem de versão e disponibilidade do scanner |
| Descrição | Antes de executar cada scanner, o orchestrator faz uma sondagem de versão com timeout dedicado (60s) para detectar binário ausente, lentidão/resource starvation e OOM kill, classificando o resultado em status distintos. |
| Prioridade | Obrigatório |
Fluxo (runOne): Supports(target) → se não suportado, skipped; caso contrário, Version(verCtx) com context.WithTimeout(ctx, ProbeTime). O resultado de erro é classificado.
Entradas: adapter, target, Options.ProbeTime (default defaultProbeTime = 60s).
Saídas: versão do scanner (em caso de sucesso) ou ScannerRun.Status = "unavailable" com uma mensagem de diagnóstico específica.
Regras de negócio — classificação da sondagem
| Condição | Status | Diagnóstico |
|---|---|---|
!Supports(target) |
skipped |
"does not support target" |
verCtx.Err() == DeadlineExceeded |
unavailable |
"version probe exceeded 60s — tool too slow / resource-starved" |
erro contém signal: killed |
unavailable |
"version probe killed — likely OOM; raise memory limit" |
| outro erro (binário ausente) | unavailable |
erro cru ("not installed/available") |
- A sondagem é generosa (60s) por design: ferramentas pesadas (ex. checkov/Python) têm cold start lento, especialmente quando os 12 scanners sobem em paralelo num runner com pouca memória. Um orçamento apertado marcaria uma ferramenta funcional como
unavailable. ProbeTime <= 0⇒ usadefaultProbeTime(60s).- Um scanner
unavailable/skippednão contribui achados, mas aparece no relatório (RF-007).
Dependências: RF-002, RF-004. Consumido por RF-005, RF-007.
RF-007 — Status por scanner (transparência de execução)¶
| Campo | Conteúdo |
|---|---|
| ID | RF-007 |
| Nome | Status por scanner (transparência de execução) |
| Descrição | Toda execução de scanner é registrada com nome, versão, status, contagem de achados, duração e erro. Esse status é exibido no resumo e embutido no relatório, para que "0 achados" nunca seja confundido com "o scan não rodou". |
| Prioridade | Obrigatório |
Estados possíveis (ScannerRun.Status):
| Status | Significado |
|---|---|
ran |
rodou com sucesso; Findings reflete a contagem |
skipped |
não suporta o tipo de alvo |
unavailable |
binário ausente, lento (timeout da sondagem) ou killed (OOM) |
error |
falhou durante a execução (não um timeout) — inclui conftest sem políticas Rego (ver RF-008) e estouro do cap de saída (RF-026) |
timeout |
execução excedeu --timeout |
Entradas: resultados de cada runOne.
Saídas: Result.Runs []ScannerRun; resumo em stderr (RF-021); bloco scanners no SARIF (properties.scanners com name/status/version) e equivalente em JSON/XML; métricas quorum_scanner_up/quorum_scanner_findings/quorum_scanner_duration_seconds (RF-023).
Regras de negócio
- Princípio "0 achados não é prova de segurança": o resumo sempre imprime a nota e os status por scanner; o SARIF inclui scannerSummary em properties.
- Mensagens de erro são truncadas em 60 caracteres no resumo de console (a versão completa fica no relatório JSON/SARIF).
Dependências: RF-005, RF-006. Consumido por RF-016, RF-021, RF-023.
RF-008 — Normalização canônica de achados¶
| Campo | Conteúdo |
|---|---|
| ID | RF-008 |
| Nome | Normalização canônica de achados |
| Descrição | Cada adapter invoca seu scanner nativo e traduz a saída para model.Finding, com tipo, severidade normalizada, identidade (CVE/PURL/RuleID/Resource/Location) e metadados. Nenhuma etapa posterior opera sobre o JSON cru do scanner. |
| Prioridade | Obrigatório |
Tipos canônicos (model.FindingType): VULN, MISCONFIG, SECRET, K8S_POSTURE, IMG_HARDENING.
Normalização de severidade (internal/severity + mapeadores locais dos adapters): converge para CRITICAL > HIGH > MEDIUM > LOW > INFO > UNKNOWN.
| Função | Mapeamento |
|---|---|
FromCVSS |
≥9.0 CRIT · ≥7.0 HIGH · ≥4.0 MED · >0 LOW · 0 UNKNOWN |
FromLabel |
enums de Trivy/Grype/Checkov/KICS/Terrascan (ex. ERROR/DANGER→HIGH, WARNING→MED, NEGLIGIBLE→INFO) |
FromDockle |
FATAL→HIGH · WARN→MED · INFO→LOW · PASS/SKIP/IGNORE→INFO |
ksSeverity (kubescape) |
derivada do scoreFactor 0..10: ≥9 CRIT · ≥7 HIGH · ≥4 MED · >0 LOW · caso contrário MED |
polarisSeverity |
danger/error→HIGH · warning→MED · caso contrário LOW |
kubeScoreSeverity |
grade 1..10 (1 pior): ≤3 HIGH · ≤7 MED · caso contrário LOW |
regulaSeverity |
critical/high/medium/low/informational; fallback FromLabel |
Matriz de cobertura por adapter (a partir de Supports/Capabilities/Run) — os 12 scanners:
| Scanner | Alvos suportados (Supports) |
Tipos produzidos (Capabilities) |
Família (consenso) |
|---|---|---|---|
| trivy | image, repo, k8s | VULN, MISCONFIG, SECRET | sca |
| grype | image, repo | VULN | sca |
| checkov | repo, k8s | MISCONFIG | iac |
| kics | repo, k8s | MISCONFIG | iac |
| terrascan | repo, k8s | MISCONFIG | iac |
| tfsec | repo, k8s | MISCONFIG | iac |
| regula | repo, k8s | MISCONFIG | iac |
| conftest | repo, k8s | MISCONFIG (policy-as-code) | policy |
| dockle | image | IMG_HARDENING | hardening |
| kubescape | k8s, repo | K8S_POSTURE | k8s |
| polaris | k8s, repo | K8S_POSTURE | k8s |
| kube-score | k8s | K8S_POSTURE | k8s |
Entradas: adapter.Target, context com timeout, QUORUM_<SCANNER>_ARGS (RF-025).
Saídas: []model.Finding com campos preenchidos por tipo (VULN: VulnID/PURL/CVSS; MISCONFIG: RuleID/CanonicalControl/Resource/Location; etc.). Quando a camada consultiva está habilitada, o MergedFinding também carrega Remediation, References, Advice (tipos model.Remediation/DocRef/Advice/Fix) — ver RF-028…RF-031; esses campos são apenas de apresentação e nunca afetam a identidade.
Regras de negócio
- Adapters nunca calculam CorrelationKey/Fingerprint — isso é centralizado no correlator (RF-014).
- Trivy emite ids AVD diretamente; normalizeAVD reconstrói o prefixo AVD- quando versões mais novas o omitem (ex. AWS-0086 → AVD-AWS-0086).
- tfsec extrai o id AVD de seus links (avdInLink) e preenche CanonicalControl — de modo que auto-correlaciona com o Trivy sem depender do crosswalk.
- dockle usa seu próprio código CIS-DI como CanonicalControl (já canônico) e só emite lacunas acionáveis (descarta PASS/SKIP/IGNORE/INFO).
- conftest não tem regras embutidas: ele avalia o seu próprio Rego (default ./policy, ou QUORUM_CONFTEST_ARGS="--policy <dir>"). Sem políticas, ele erra e o scanner é reportado como error — comportamento esperado, policy-as-code é opt-in. O RuleID é fixado como policy (Rego não tem id estável), mantendo cada achado isolado; falhas→HIGH, avisos→MED.
- regula/polaris/kubescape/kube-score emitem apenas resultados de falha (FAIL/success:false/status failed/grade < 10).
- Confirmed é setado quando o achado tem fonte autoritativa (ex. Trivy com DataSource, Grype com um CVE em relatedVulnerabilities), influenciando o consenso (RF-015).
- Segredos do Trivy são redigidos antes do armazenamento: só um trecho do Match sobrevive, com tokens longos mascarados (redactSecretText) — o valor cru do segredo nunca é persistido.
- Cada adapter tem um teste de contrato contra fixtures em internal/adapter/testdata.
Dependências: RF-002, RF-005, RF-025. Alimenta RF-009, RF-012, RF-014.
RF-009 — Resolução de aliases de vulnerabilidade¶
| Campo | Conteúdo |
|---|---|
| ID | RF-009 |
| Nome | Resolução de aliases de vulnerabilidade |
| Descrição | Para achados do tipo VULN, o identificador é resolvido a uma forma canônica (CVE preferido) usando uma cadeia em camadas: aliases locais do scanner → cache local → OSV.dev. Garante que GHSA-xxxx (Grype) e CVE-yyyy (Trivy) para o mesmo bug se correlacionem em vez de se dividirem. |
| Prioridade | Obrigatório |
Fluxo da cadeia (chainResolver.Canonical)
flowchart TD
A["id + aliases do scanner"] --> B{"já tem CVE?"}
B -->|Sim| Z["retorna CVE (upper)"]
B -->|Não| C{"cache local tem o id?"}
C -->|Sim| Y["retorna valor em cache"]
C -->|Não| D{"OSV habilitado (online)?"}
D -->|Sim| E["OSV.Aliases(id) → preferCVE"]
D -->|Não| F["preferCVE(aliases locais)"]
E --> G["grava no cache + retorna"]
F --> G
Entradas: VulnID, Aliases, context; cliente OSV (quando online), *cache.Store.
Saídas: VulnID canônico (CVE > GHSA > primeiro não-vazio).
Regras de negócio
- Preferência: CVE > GHSA > primeiro id não-vazio (preferCVE).
- A cadeia nunca retorna erro — em qualquer falha (rede/HTTP), degrada para o melhor id disponível (DESIGN §7).
- OSV (api.osv.dev/v1/vulns/{id}) usa um cliente com timeout de 8s, MaxRetries=2 (backoff exponencial, base de 200ms); apenas 429/5xx/erros de rede são retryable. O id é validado e passado por url.PathEscape antes de compor a URL.
- Resultados resolvidos são gravados no cache (RF-011) para idempotência em re-scans de CI.
- Apenas VULN passa por aliasing; os demais tipos usam o crosswalk (RF-012).
Dependências: RF-008, RF-010 (offline), RF-011 (cache). Alimenta RF-014.
RF-010 — Modo offline (OSV desabilitado)¶
| Campo | Conteúdo |
|---|---|
| ID | RF-010 |
| Nome | Modo offline (OSV desabilitado) |
| Descrição | A flag --offline desabilita todas as consultas de rede à OSV.dev; a resolução de aliases então usa apenas os aliases locais do scanner e o cache. |
| Prioridade | Obrigatório |
Fluxo: em runScan, if !f.offline { osv = alias.NewOSVClient() }; passar nil a alias.New pula a Camada 3 (OSV).
Entradas: --offline (bool, default false).
Saídas: resolver que opera apenas com as Camadas 1 e 2.
Regras de negócio
- --offline ⇒ nenhuma chamada HTTP é feita; os aliases dependem só do que o scanner reportou + o cache existente.
- O estado offline é logado: ... offline=%v.
- Recomendado em ambientes air-gapped e para builds reproduzíveis. Nota: --offline também bloqueia --advice-provider=remote (RF-030).
Dependências: RF-009.
RF-011 — Cache de aliases¶
| Campo | Conteúdo |
|---|---|
| ID | RF-011 |
| Nome | Cache de aliases |
| Descrição | Um armazenamento chave/valor persistente em arquivo JSON acelera e dá idempotência à resolução de aliases em re-scans, sem dependência de banco de dados. |
| Prioridade | Recomendado |
Entradas: --cache (caminho; default os.UserCacheDir()/quorum/aliases.json, fallback .quorum-cache.json).
Saídas: arquivo JSON {schemaVersion, id: canonical} atualizado a cada Put, escrito com permissão 0600.
Regras de negócio
- Arquivo ausente/ilegível ⇒ cache vazio, nunca um erro (é uma otimização, não uma fonte de falha).
- O cache carrega um schemaVersion: entradas de um schema incompatível são descartadas (o cache reinicia limpo) em vez de misturar formatos.
- Escrita atômica via *.tmp + os.Rename, com permissão 0600 (o arquivo pode conter ids de vulnerabilidade); falhas de flush são silenciosamente ignoradas.
- Seguro para uso concorrente intra-processo (sync.RWMutex).
- Sem TTL/expiração: as entradas persistem (premissa — ver Premissas).
Dependências: RF-009.
RF-012 — Crosswalk regra → controle canônico¶
| Campo | Conteúdo |
|---|---|
| ID | RF-012 |
| Nome | Crosswalk regra → controle canônico |
| Descrição | Para achados MISCONFIG/K8S_POSTURE/IMG_HARDENING, o id de regra nativo de cada scanner é mapeado para um controle canônico compartilhado (AVD, com fallback por categoria semântica), via arquivos YAML, para que misconfigs equivalentes de engines diferentes se correlacionem. |
| Prioridade | Obrigatório |
Fluxo (crosswalk.Load + Correlator.resolveControl): carrega todo *.yaml/*.yml de um diretório, indexa "scanner|ruleID" → Resolution{Control, Category, CWE, Title}. Durante o enriquecimento, se o achado já tiver CanonicalControl (ex. AVD do Trivy/tfsec, CIS-DI do dockle), ele o mantém; caso contrário, resolve por RuleID.
Crosswalk empacotado (./crosswalk, derivado da saída real dos scanners, sob o princípio false split > false merge):
| Arquivo | Hub canônico | Cobertura |
|---|---|---|
crosswalk/aws.yaml |
AVD (AWS) | S3, IAM, EBS, Security Groups, RDS, KMS, CloudTrail, VPC flow logs |
crosswalk/azure.yaml |
AVD (Azure) | Storage, Key Vault |
crosswalk/gcp.yaml |
AVD (GCP) | bucket, firewall, SQL |
crosswalk/k8s.yaml |
C-#### (kubescape) | privilege-escalation, privileged, non-root, limits de cpu/mem, probes, read-only-fs, linux-hardening, automount-SA, network-policy, host-network, host-PID/IPC, capabilities, secrets |
- Os hubs AWS/Azure/GCP correlacionam checkov × kics × terrascan × tfsec × regula × trivy; o hub k8s correlaciona kubescape × polaris × kube-score.
tfsec/trivyjá emitem AVD nativo e, portanto, auto-correlacionam sem uma entrada de crosswalk. - RBAC permanece single-engine: a análise de RBAC do kubescape exige contexto de cluster, então não é correlacionada com outros engines (documentado, não é uma lacuna do crosswalk).
Entradas: --crosswalk (diretório; default ./crosswalk), Scanner, RuleID.
Saídas: CanonicalControl (+ Category/Title) preenchido, ou Unmapped=true.
Regras de negócio
- Um diretório ausente não é erro (os.IsNotExist) — a ferramenta roda sem crosswalk customizado, e os achados ficam Unmapped.
- "Nunca adivinhe um match" (DESIGN §6): sem um mapeamento, o achado é isolado e marcado Unmapped=true; nunca é mesclado por chute.
- O mapeamento é case-insensitive no nome do scanner; a chave é lower(scanner)|trim(ruleID).
- Um achado já canônico (CanonicalControl != "") não é re-resolvido.
Dependências: RF-008, RF-013 (fallback de diretório). Alimenta RF-014.
RF-013 — Fallback automático do crosswalk empacotado¶
| Campo | Conteúdo |
|---|---|
| ID | RF-013 |
| Nome | Fallback automático do crosswalk empacotado |
| Descrição | Quando --crosswalk não é passado explicitamente e o default ./crosswalk não existe, o sistema usa o crosswalk empacotado na imagem Docker em /opt/quorum/crosswalk, evitando carregar silenciosamente 0 regras em docker run. |
| Prioridade | Recomendado |
Fluxo (resolveCrosswalkDir)
| Condição | Diretório usado |
|---|---|
--crosswalk passado explicitamente (Changed) |
o valor literal fornecido |
default ./crosswalk existe |
./crosswalk |
default ausente e /opt/quorum/crosswalk existe |
/opt/quorum/crosswalk (empacotado) |
| nenhum existe | retorna o default (carrega 0 regras) |
Entradas: --crosswalk, a flag Changed do cobra, existência do diretório (os.Stat).
Saídas: diretório de crosswalk efetivo (logado: crosswalk=%d rules (%s)).
Regras de negócio
- O fallback ocorre somente quando o usuário não alterou --crosswalk (respeita uma escolha explícita ao pé da letra).
- Resolve o problema de docker run … scan . a partir de um workdir arbitrário onde ./crosswalk não existe.
Dependências: RF-012.
RF-014 — Correlação por correlationKey e fingerprint¶
| Campo | Conteúdo |
|---|---|
| ID | RF-014 |
| Nome | Correlação por correlationKey e fingerprint |
| Descrição | Cada achado recebe um correlationKey determinístico específico por tipo e um Fingerprint = sha256(correlationKey). A chave é a identidade usada para agrupar achados equivalentes no consenso e para supressão/portabilidade no SARIF. |
| Prioridade | Obrigatório |
Estratégias de chave por tipo (BuildKey):
| Tipo | Estrutura do correlationKey |
|---|---|
VULN |
VULN\|UPPER(VulnID)\|nameVersion(PURL) |
MISCONFIG |
MISCONFIG\|fileBasename\|resourceType\|controlKey |
K8S_POSTURE |
K8S\|ns/kind/name\|container\|controlKey |
IMG_HARDENING |
IMGH\|controlKey |
SECRET |
SECRET\|normPath\|startLine\|lower(RuleID) |
| outros | OTHER\|scanner\|title |
Entradas: model.Finding enriquecido (após alias/crosswalk).
Saídas: f.CorrelationKey, f.Fingerprint.
Regras de negócio
- controlKey prefere CanonicalControl; quando Unmapped, usa UNMAPPED:scanner:RuleID para nunca mesclar um achado não mapeado com outro diferente (false split > false merge).
- MISCONFIG chaveia pelo basename do arquivo + tipo do recurso + controle (engines reportam caminhos/relatividades diferentes). Trade-off documentado: dois recursos distintos do mesmo tipo, mesmo controle e mesmo arquivo podem over-merge — aceitável frente a nunca correlacionar.
- A chave é uma função pura e determinística dos dados normalizados.
- O Fingerprint é exposto no SARIF como partialFingerprints["quorum/v1"] (RF-016) e aceito pelo baseline (RF-017).
- Sem um correlator, o orchestrator ainda aplica BuildKey/Fingerprint para viabilizar o agrupamento.
Dependências: RF-009, RF-012. Alimenta RF-015, RF-016, RF-017.
RF-015 — Pontuação e agregação de consenso¶
| Campo | Conteúdo |
|---|---|
| ID | RF-015 |
| Nome | Pontuação e agregação de consenso |
| Descrição | Achados com o mesmo correlationKey são agrupados num MergedFinding, com severidade agregada (máxima), a lista de scanners que o detectaram, detectionCount e uma confidence (0..1) que pondera contagem, diversidade de engines, severidade e confirmação autoritativa. |
| Prioridade | Obrigatório |
Fórmula de confiança (consensus.confidence, DESIGN §9):
| Fator | Cálculo |
|---|---|
count (0.35) |
ln(1+nScanners)/ln(5) — retornos decrescentes |
diversity (0.25) |
famílias de engine distintas: 1→0.33, 2→0.66, 3+→1.0 |
severity (0.25) |
CRIT 1.0 · HIGH 0.8 · MED 0.5 · LOW 0.3 · INFO 0.1 |
authoritative (0.15) |
1.0 se algum membro for Confirmed ou um CVE com CVSS>0; caso contrário 0 |
Famílias de engine (scannerCategory): sca = trivy/grype · iac = checkov/kics/terrascan/tfsec/regula · policy = conftest · k8s = kubescape/polaris/kube-score · hardening = dockle. Um scanner fora do mapa recebe a família other:<name> (isolado).
Entradas: []model.Finding com CorrelationKey.
Saídas: []model.MergedFinding (Title, Severity máxima, DetectedBy, DetectionCount, Confidence, Unmapped, Members, Fingerprint), ordenado por (severidade ↓, confiança ↓, detectionCount ↓, chave ↑). A camada consultiva pode, mais tarde, anexar Remediation/References/Advice a um MergedFinding (RF-028…RF-031) sem alterar nenhum desses campos de consenso.
Regras de negócio
- DetectionCount = número de scanners distintos (não achados crus).
- A diversidade de engines pesa mais do que a repetição da mesma família: 2 engines de famílias diferentes valem mais do que 2 do mesmo grupo (ex. checkov + terrascan, ambos iac, contam como 1 família).
- Unmapped propaga se algum membro for não mapeado.
- A severidade agregada é a máxima entre os membros (severity.Max).
- Ordenação estável e determinística para relatórios reproduzíveis.
Dependências: RF-014. Alimenta RF-016, RF-017, RF-018, RF-019, RF-023.
RF-016 — Emissão de relatório SARIF/JSON/XML¶
| Campo | Conteúdo |
|---|---|
| ID | RF-016 |
| Nome | Emissão de relatório SARIF/JSON/XML |
| Descrição | O resultado consolidado é serializado em um de três formatos: SARIF 2.1.0 (primário), JSON ou XML. O SARIF carrega fingerprints portáveis, propriedades de consenso e o resumo dos scanners. |
| Prioridade | Obrigatório |
Fluxo (report.ParseFormat + report.Write): valida --format e despacha para writeSARIF/writeJSON/writeXML.
Entradas: --format/-f (sarif|json|xml, default sarif), *orchestrator.Result.
Saídas — SARIF (primário):
| Elemento SARIF | Origem |
|---|---|
runs[].tool.driver |
nome quorum, version (build-time), rules por achado |
results[].ruleId |
CVE (VULN) / CanonicalControl / RuleID / correlationKey |
results[].level |
CRIT/HIGH→error, MED→warning, outros→note |
results[].partialFingerprints["quorum/v1"] |
Fingerprint (sha256) |
results[].properties |
detectedBy, detectionCount, confidence, severity, correlationKey, unmapped |
runs[].properties.scanners |
resumo name/status/version (RF-007) |
Regras de negócio
- --format inválido ⇒ erro unknown format (exit 2), antes de qualquer scanner rodar.
- O SARIF usa o schema 2.1.0; SetEscapeHTML(false) para preservar caracteres em URIs.
- As rules SARIF são deduplicadas por ruleId e ordenadas.
- O JSON inclui o detalhe canônico cru (Result.Findings); o XML é a forma estruturada alternativa.
- Quando --advice está ligado, o relatório também renderiza os anexos consultivos (remediação, referências OWASP, recomendação de IA), cada um rotulado "AI-generated, advisory only" onde aplicável (RF-028…RF-031).
Dependências: RF-015, RF-007. Consumido por RF-020.
RF-017 — Baseline de supressão (.quorumignore)¶
| Campo | Conteúdo |
|---|---|
| ID | RF-017 |
| Nome | Baseline de supressão (.quorumignore) |
| Descrição | Um arquivo de baseline lista achados conhecidos/aceitos a suprimir, por Fingerprint ou correlationKey (um por linha). Achados suprimidos são removidos do relatório e do gating, mas a supressão é sempre logada. |
| Prioridade | Obrigatório |
Fluxo (filter.LoadBaseline + Baseline.Has + filter.Apply): carrega ids (lowercase), ignora linhas em branco e comentários # (inclusive # nota ao final); um MergedFinding casa se seu fingerprint OU correlationKey estiver no conjunto.
Entradas: --baseline (caminho; default .quorumignore), MergedFinding.
Saídas: lista filtrada (Result.Kept) + contador SuppressedBaseline.
Regras de negócio
- Arquivo ausente: se --baseline não foi alterado, baseline vazio (silencioso); se foi passado explicitamente e não existe ⇒ erro baseline file not found (exit 2).
- Casa por Fingerprint ou correlationKey — o usuário pode copiar qualquer um do relatório.
- Comparação case-insensitive.
- Supressões são logadas (filtered: %d suppressed by baseline ...) — "um achado suprimido ainda é um achado" (DESIGN §14).
Dependências: RF-014, RF-015.
RF-018 — Filtro de severidade mínima (--min-severity)¶
| Campo | Conteúdo |
|---|---|
| ID | RF-018 |
| Nome | Filtro de severidade mínima (--min-severity) |
| Descrição | Achados abaixo da severidade mínima fornecida são removidos do relatório e do gating, reduzindo ruído no CI. |
| Prioridade | Obrigatório |
Fluxo (severity.Parse + filter.Apply): achados com severity.AtLeast(m.Severity, minSeverity) == false são descartados (quando minSeverity != UNKNOWN).
Entradas: --min-severity (critical|high|medium|low).
Saídas: lista filtrada + contador SuppressedSeverity.
Regras de negócio
- Valor inválido ⇒ erro invalid --min-severity (exit 2).
- Ausente ⇒ SevUnknown ⇒ filtro desabilitado (mantém tudo).
- Aplicado depois do consenso e antes do gating (RF-019), portanto afeta o código de saída.
- A contagem de descartes é logada junto com a supressão de baseline.
Dependências: RF-015, internal/severity.
RF-019 — Gate de falha (--fail-on) e códigos de saída¶
| Campo | Conteúdo |
|---|---|
| ID | RF-019 |
| Nome | Gate de falha (--fail-on) e códigos de saída |
| Descrição | Quando --fail-on é fornecido, o processo sai com código 1 se algum achado (após os filtros) atingir ou exceder a severidade limite. Os códigos de saída são o contrato de integração com CI/CD. |
| Prioridade | Obrigatório |
Contrato de código de saída
| Exit | Significado |
|---|---|
0 |
OK — sucesso, ou nenhum achado atingiu --fail-on |
1 |
Gate disparou — algum achado ≥ --fail-on |
2 |
Erro de uso/execução (flag inválida, baseline explícito ausente, alvo inválido, cap de tamanho excedido, erro de orquestração) |
Fluxo: após emitir o relatório, o resumo e as métricas, calcula worstSeverity(res); se severity.AtLeast(worst, failThreshold) ⇒ os.Exit(1).
Entradas: --fail-on (critical|high|medium|low).
Saídas: código de saída; log gate: found %s finding >= --fail-on %s → exit 1.
Regras de negócio
- Valor inválido ⇒ erro invalid --fail-on (exit 2).
- O gate considera apenas achados que sobreviveram ao baseline (RF-017) e à severidade mínima (RF-018).
- Sem --fail-on, o scan nunca retorna 1 por causa de achados (exit 0), apenas 2 em caso de erro.
- A camada consultiva nunca afeta o gate: --advice/--fix são apenas de apresentação e não podem alterar worstSeverity nem o código de saída (RF-028…RF-031).
- Exit 2 é produzido por main.go quando Execute() retorna um erro.
Dependências: RF-015, RF-017, RF-018.
RF-020 — Saída para arquivo ou stdout¶
| Campo | Conteúdo |
|---|---|
| ID | RF-020 |
| Nome | Saída para arquivo ou stdout |
| Descrição | O relatório é escrito em stdout por padrão ou em arquivo via --output/-o; o caminho é normalizado e diretórios intermediários são criados automaticamente. |
| Prioridade | Obrigatório |
Fluxo (emit): renderiza para um buffer; se --output estiver vazio ⇒ cmd.OutOrStdout(); caso contrário filepath.Clean(output) + os.MkdirAll(dir, 0o755) + os.WriteFile com permissão 0600.
Entradas: --output/-o (caminho; default stdout).
Saídas: relatório no destino escolhido.
Regras de negócio
- O caminho é normalizado com filepath.Clean (colapsa segmentos ./ e ../).
- O diretório do arquivo é criado com os.MkdirAll(..., 0o755) se não existir.
- O relatório é escrito com permissão 0600 (somente dono): pode conter detalhe sensível de achados, portanto não deve ser world-readable por padrão.
- stdout permite piping; o resumo e os logs vão para stderr (RF-021), nunca poluindo a saída do relatório.
Dependências: RF-016.
RF-021 — Resumo no console e logs de progresso¶
| Campo | Conteúdo |
|---|---|
| ID | RF-021 |
| Nome | Resumo no console e logs de progresso |
| Descrição | Durante e após o scan, o Quorum imprime logs de progresso e um resumo final em stderr com status por scanner, contagens de severidade, achados multi-detectados, tempo decorrido e a nota de segurança. --quiet/-q suprime essa saída. |
| Prioridade | Recomendado |
Entradas: --quiet/-q (bool), --log-format (RF-024), *orchestrator.Result.
Saídas (stderr): linhas de progresso (formato text [quorum] ... ou json, conforme RF-024) e um bloco ── quorum summary ── com:
- status/versão/achados por scanner;
- total após o consenso e quantos são multi-detectados (DetectionCount > 1);
- contagens CRIT/HIGH/MED/LOW/INFO;
- tempo decorrido;
- nota: "0 findings is not proof of safety — see scanner statuses above."
Regras de negócio
- --quiet suprime ambos os logs de progresso e o resumo (mas não o relatório do RF-020 nem o código de saída).
- Todos os logs vão para stderr, mantendo o stdout limpo para o relatório.
- O bloco ── quorum summary ── é sempre texto tabular (não muda com --log-format); apenas as linhas de progresso logf honram text|json.
Dependências: RF-007, RF-015, RF-024.
RF-022 — Versão da ferramenta¶
| Campo | Conteúdo |
|---|---|
| ID | RF-022 |
| Nome | Versão da ferramenta |
| Descrição | O Quorum reporta sua versão (a flag --version do cobra), sobrescrita em tempo de build via -ldflags "-X main.version=...", também carimbada no driver SARIF. |
| Prioridade | Recomendado |
Entradas: --version (cobra), variável version (build-time).
Saídas: string de versão em stdout; Version propagado para report.Version (driver SARIF e namespace quorum/v1).
Regras de negócio
- O default em código é 0.1.0; releases o sobrescrevem via GoReleaser/ldflags (o release atual é v0.8.3).
Dependências: nenhuma.
RF-023 — Exportação de métricas Prometheus (--metrics)¶
| Campo | Conteúdo |
|---|---|
| ID | RF-023 |
| Nome | Exportação de métricas Prometheus (--metrics) |
| Descrição | Quando --metrics <file> é fornecido, o Quorum escreve métricas no formato textfile do Prometheus (para o textfile collector do Node Exporter ou push gateway), habilitando observabilidade das execuções de scan no CI. |
| Prioridade | Recomendado |
Fluxo (writeMetricsFile → report.WriteMetrics): após emitir o relatório e o resumo, se --metrics não estiver vazio, o caminho passa por filepath.Clean, o diretório é criado (0o755) e o arquivo é escrito com permissão 0644 (métricas são contagens não-sensíveis, destinadas a scraping). Uma falha de escrita ⇒ erro writing metrics (exit 2).
Entradas: --metrics (caminho; default vazio = desabilitado), *orchestrator.Result, métricas consultivas opcionais.
Saídas (arquivo texto Prometheus):
| Métrica | Tipo | Descrição |
|---|---|---|
quorum_scan_duration_seconds |
gauge | tempo total (wall-clock) do scan |
quorum_scanner_up{scanner,status} |
gauge | 1 se status == ran, senão 0 (skipped/unavailable/error/timeout) |
quorum_scanner_findings{scanner} |
gauge | achados crus por scanner (pré-consenso) |
quorum_scanner_duration_seconds{scanner} |
gauge | duração por scanner |
quorum_findings_after_consensus |
gauge | achados remanescentes após o merge de consenso |
quorum_advice_enriched{kind} |
gauge | anexos consultivos, kind=remediation\|references\|recommendation (apenas sob --advice) |
quorum_advice_provider{provider} |
gauge | 1 para o provedor de IA ativo (apenas quando --advice-provider é local/remote) |
quorum_advice_fix{stage} |
gauge | correções sugeridas por estágio verify-the-fix, stage=proposed\|verified (apenas sob --fix=suggest) |
Regras de negócio
- --metrics vazio ⇒ nenhum arquivo é escrito (feature opt-in).
- As métricas são escritas depois do relatório e do resumo, mas antes do gate (RF-019), de modo que existem mesmo quando o processo termina com exit 1.
- Permissão 0644 (não-sensível), em contraste com o relatório 0600 (RF-020).
- As séries quorum_advice_* são emitidas apenas quando --advice está habilitado; sem ele o arquivo de métricas é byte-a-byte idêntico ao de uma execução não-consultiva.
Dependências: RF-007, RF-015, RF-028…RF-031. Componente: internal/report/metrics.go.
RF-024 — Formato dos logs de progresso (--log-format)¶
| Campo | Conteúdo |
|---|---|
| ID | RF-024 |
| Nome | Formato dos logs de progresso (--log-format) |
| Descrição | As linhas de progresso em stderr podem ser emitidas em texto legível ([quorum] ...) ou JSON estruturado ({ts, level, msg}), para ingestão por coletores de log em pipelines de CI. |
| Prioridade | Recomendado |
Fluxo (o closure logf em runScan): se --quiet, não emite nada; se --log-format json, serializa {ts (RFC3339 UTC), level:"info", msg} e imprime uma linha JSON; caso contrário imprime [quorum] <msg>.
Entradas: --log-format (string; default text; aceita text|json).
Saídas: linhas de progresso em stderr no formato escolhido.
Regras de negócio
- Valor inválido (≠ text/json) ⇒ erro invalid --log-format (exit 2), validado logo após resolver o tipo do alvo.
- Aplica-se apenas às linhas de progresso logf; o bloco ── quorum summary ── (RF-021) permanece tabular.
- --quiet tem precedência: nenhum log é emitido, independentemente de --log-format.
Dependências: RF-021.
RF-025 — Passthrough de argumentos por scanner (QUORUM_<SCANNER>_ARGS)¶
| Campo | Conteúdo |
|---|---|
| ID | RF-025 |
| Nome | Passthrough de argumentos por scanner |
| Descrição | O operador pode injetar argumentos de CLI extras em um scanner específico via a variável de ambiente QUORUM_<NAME>_ARGS, sem modificar o adapter — para ampliar a cobertura (frameworks/checks extras) ou passar credenciais de plataforma. |
| Prioridade | Recomendado |
Fluxo (extraArgs + splitArgs em adapter.go): cada adapter, ao montar sua linha de comando, faz args = append(args, extraArgs("<name>")...) antes de target.Ref. extraArgs lê QUORUM_<UPPER(NAME)>_ARGS, e splitArgs divide o valor no estilo shell (por espaços, honrando aspas simples e duplas; sem escaping ou expansão de variáveis).
Entradas: variáveis de ambiente QUORUM_TRIVY_ARGS, QUORUM_CHECKOV_ARGS, QUORUM_CONFTEST_ARGS, etc. (uma por scanner registrado).
Saídas: argumentos extras concatenados à invocação nativa do scanner.
Regras de negócio
- O valor é controlado pelo operador (quem roda o container/CLI) — o mesmo nível de confiança que os próprios flags.
- Exemplos de uso: QUORUM_CHECKOV_ARGS="--bc-api-key <key>" desbloqueia políticas Prisma Cloud/Bridgecrew; QUORUM_CONFTEST_ARGS="--policy <dir>" aponta o conftest para seu Rego; QUORUM_TRIVY_ARGS amplia frameworks/scanners.
- A divisão honra aspas: --policy "/path with space" vira um único argumento.
- Ausência/valor vazio ⇒ nenhum argumento extra (nil).
Dependências: RF-008. Componente: internal/adapter/adapter.go (extraArgs, splitArgs).
RF-026 — Caps de DoS: saída e tamanho do alvo¶
| Campo | Conteúdo |
|---|---|
| ID | RF-026 |
| Nome | Caps de DoS: saída e tamanho do alvo |
| Descrição | Dois caps protegem o processo contra exaustão de memória/recursos: o volume de stdout bufferizado por scanner e o tamanho em disco de um alvo de filesystem. Ambos têm defaults altos e são ajustáveis/desabilitáveis via variável de ambiente. |
| Prioridade | Obrigatório |
Cap de saída do scanner (capWriter + maxOutputBytes em adapter.go)
| Aspecto | Valor |
|---|---|
| Default | 512 MiB (defaultMaxOutputBytes) |
| Override | QUORUM_MAX_OUTPUT_BYTES (bytes; deve ser > 0) |
| Comportamento | capWriter bufferiza até o limite e descarta o restante sem bloquear o pipe do processo filho; ao final, se houve estouro, runCmd retorna um erro output exceeded N bytes — aborting to avoid OOM e o scanner vira error (RF-007) |
Cap de tamanho do alvo (checkTargetSize + defaultMaxTargetBytes em scan.go)
| Aspecto | Valor |
|---|---|
| Default | 20 GiB (defaultMaxTargetBytes) |
| Override | QUORUM_MAX_TARGET_BYTES (bytes; 0 desabilita) |
| Escopo | apenas alvos repo/k8s (locais); image é pulado (sem árvore local) |
| Comportamento | filepath.WalkDir soma o tamanho dos arquivos regulares e para assim que o cap é cruzado (repos normais pagam só um stat pass leve); um estouro ⇒ erro target %q exceeds the N-byte size cap (exit 2) |
Entradas: QUORUM_MAX_OUTPUT_BYTES, QUORUM_MAX_TARGET_BYTES, target.
Saídas: abort limpo (erro) quando um cap é excedido.
Regras de negócio
- Valor inválido de QUORUM_MAX_TARGET_BYTES (não-inteiro) ⇒ erro invalid QUORUM_MAX_TARGET_BYTES.
- QUORUM_MAX_TARGET_BYTES <= 0 ⇒ cap desabilitado.
- Valor inválido/≤0 de QUORUM_MAX_OUTPUT_BYTES ⇒ ignorado (mantém o default de 512 MiB).
- Entradas ilegíveis durante o walk são puladas (não abortam o scan).
- O cap de alvo roda antes do fan-out; o cap de saída roda durante cada runCmd.
Dependências: RF-002 (tipo do alvo), RF-005 (execução). Componentes: internal/adapter/adapter.go, cmd/quorum/scan.go.
RF-027 — Validação do alvo (injeção de argumentos)¶
| Campo | Conteúdo |
|---|---|
| ID | RF-027 |
| Nome | Validação do alvo (injeção de argumentos) |
| Descrição | O target posicional é validado antes de ser passado a qualquer scanner: um valor começando com - é rejeitado, pois um scanner downstream poderia interpretá-lo como flag de CLI (argument injection). |
| Prioridade | Obrigatório |
Fluxo (validateTargetRef em scan.go, chamado no início de runScan): se strings.HasPrefix(ref, "-"), retorna erro; caso contrário, prossegue.
Entradas: <target> (argumento posicional).
Saídas: erro invalid target %q: must not start with '-' (exit 2), sugerindo ./-name para um caminho literal; ou prossegue.
Regras de negócio
- Nenhuma ref de imagem real nem caminho legítimo começa com -; tal caminho deve ser passado como ./-name.
- A validação é o primeiro passo de runScan, antes da inferência de tipo, dos caps e do fan-out.
Dependências: RF-001. Precede RF-002.
RF-028 — Enriquecimento consultivo (--advice, knowledge pack)¶
| Campo | Conteúdo |
|---|---|
| ID | RF-028 |
| Nome | Enriquecimento consultivo (--advice, knowledge pack) — Fase 0 |
| Descrição | Com --advice, o Quorum anexa templates de remediação determinísticos e referências OWASP (Fase 0, sem modelo) aos achados mesclados, casados por canonicalControl/ruleId/category/type a partir de um knowledge pack curado. É apenas de apresentação e nunca toca em identidade, confiança, severidade agregada ou no gate. |
| Prioridade | Recomendado |
Fluxo (internal/enrich + runScan): carrega o knowledge pack de --knowledge (default ./knowledge, com um fallback empacotado análogo ao do crosswalk, para que docker run … scan . --advice de qualquer workdir ainda encontre os templates), depois, para cada MergedFinding, busca um template correspondente e preenche Remediation/References. Logado: advice: knowledge=%d entries (%s) → %d findings enriched.
Knowledge pack (knowledge/*.yaml): templates de remediação curados + referências OWASP organizados por domínio (aws, azure, gcp, k8s, image, categories).
Entradas: --advice (bool, default false), --knowledge (diretório; default ./knowledge), os []MergedFinding do consenso.
Saídas: MergedFinding.Remediation (model.Remediation) e MergedFinding.References ([]model.DocRef) preenchidos para achados casados; renderizados no relatório (RF-016) e contados em quorum_advice_enriched{kind=remediation|references} (RF-023).
Regras de negócio
- Apenas de apresentação: nunca modifica correlationKey, fingerprint, confidence, severidade agregada ou o gate --fail-on. Sem --advice, a saída é byte-a-byte idêntica.
- Totalmente determinístico — nenhum modelo envolvido na Fase 0; o casamento é um lookup puro por controle canônico/regra/categoria/tipo.
- Degradação graciosa: um knowledge pack ausente/ilegível não gera anexos e nunca faz o scan falhar.
Dependências: RF-015. Alimenta RF-016, RF-023. Componente: internal/enrich/enrich.go.
RF-029 — Referências OWASP via RAG e advise-index¶
| Campo | Conteúdo |
|---|---|
| ID | RF-029 |
| Nome | Referências OWASP via RAG e advise-index — Fase 2 |
| Descrição | Com --advice, o Quorum também recupera referências OWASP de um corpus versionado e fixado por digest (RAG-as-artifact, determinístico). A recuperação é lexical por padrão (sem modelo); torna-se semântica (embeddings) uma vez que o corpus seja embarcado via o subcomando advise-index. O scan seleciona automaticamente a recuperação semântica quando o corpus traz vetores. |
| Prioridade | Recomendado |
Fluxo (internal/rag + runScan): carrega knowledge/owasp/corpus.yaml; se ele tiver HasEmbeddings() e --advice-provider=local, usa um embedder semântico (--advice-embed-model) contra o endpoint local; caso contrário, faz fallback para a recuperação lexical. Logado: advice(rag): corpus=%d chunks (…) → %d findings gained OWASP references.
Subcomando advise-index (cmd/quorum/advise_index.go): lê o corpus OWASP, embarca cada chunk via um endpoint local de embeddings compatível com OpenAI e o reescreve com vetores por chunk. Os embeddings são excluídos do digest de conteúdo, então o pin é preservado. Uma vez presentes os embeddings, scan --advice --advice-provider local usa recuperação semântica automaticamente.
| Flag | Default | Significado |
|---|---|---|
--corpus |
knowledge/owasp/corpus.yaml |
arquivo de corpus a embarcar |
--out |
(vazio) | arquivo de saída (default: sobrescreve --corpus no lugar) |
--advice-endpoint |
http://localhost:11434/v1 |
base URL de embeddings compatível com OpenAI |
--advice-embed-model |
nomic-embed-text |
id do modelo de embedding |
Entradas: --advice, --advice-embed-model, o corpus OWASP; para advise-index, --corpus/--out/--advice-endpoint/--advice-embed-model.
Saídas: References OWASP anexadas aos achados casados; advise-index escreve um corpus embarcado, ainda fixado.
Regras de negócio
- Determinístico e apenas de apresentação, como o RF-028: nunca afeta identidade/confiança/gating.
- O corpus é fixado por digest; advise-index preserva o pin porque os vetores são excluídos do digest.
- A recuperação lexical default não requer modelo; a recuperação semântica só é usada quando o corpus já carrega embeddings.
- Supply chain: o knowledge pack + crosswalk recebem uma atestação de build-provenance SLSA a cada release (job knowledge do release.yml); verifique com gh attestation verify knowledge/owasp/corpus.yaml.
Dependências: RF-028. Alimenta RF-016, RF-023. Componente: internal/rag/*.go, cmd/quorum/advise_index.go.
RF-030 — Recomendações de IA (--advice-provider)¶
| Campo | Conteúdo |
|---|---|
| ID | RF-030 |
| Nome | Recomendações de IA (--advice-provider) — Fases 1 e 3 |
| Descrição | Opcionalmente, --advice-provider=local consulta um endpoint on-host compatível com OpenAI (ex. Ollama) por uma recomendação em linguagem natural por achado; --advice-provider=remote chama uma API externa (Fase 3), que é egress-gated. Todo anexo de IA é rotulado "AI-generated, advisory only" e nunca afeta o gate. |
| Prioridade | Opcional |
Fluxo (internal/advisor + runScan): quando --advice e o provider ∈ {local, remote}, monta um cliente (NewLocalClient/NewRemoteClient) e pede uma recomendação para cada achado mesclado (limitado por --advice-max). Os resultados preenchem MergedFinding.Advice. Reproduzível via temperature=0 + um cache em disco (--advice-cache) chaveado por fingerprint+provider+model.
| Flag | Default | Significado |
|---|---|---|
--advice-provider |
none |
none\|local\|remote |
--advice-endpoint |
http://localhost:11434/v1 |
base URL compatível com OpenAI |
--advice-model |
qwen2.5-coder:7b |
id do modelo para local |
--advice-embed-model |
nomic-embed-text |
modelo de embedding para recuperação OWASP semântica |
--advice-cache |
(diretório de cache do usuário) | arquivo de cache para advice de IA |
--advice-max |
50 |
máx. de achados enviados ao provider por execução (0 = sem cap) |
--advice-allow-egress |
false |
consentimento para enviar achados off-host: obrigatório para remote |
Entradas: --advice, --advice-provider, --advice-endpoint, --advice-model, --advice-cache, --advice-max, --advice-allow-egress, QUORUM_ADVICE_API_KEY (remote).
Saídas: MergedFinding.Advice (model.Advice) preenchido; métrica quorum_advice_provider{provider} (RF-023).
Regras de negócio
- Desligado por padrão e apenas de apresentação: nunca toca em correlationKey/fingerprint/confidence/severidade agregada/o gate.
- Degradação graciosa: se o modelo estiver inacessível, o relatório sai sem advice de IA e o scan nunca falha.
- Provedor remoto (Fase 3) envia apenas o achado normalizado (títulos, caminhos, controles) — nunca código-fonte. Ele é:
- BLOQUEADO por --offline (um modelo remoto enviaria achados off-host);
- condicionado a consentimento explícito via --advice-allow-egress;
- requer QUORUM_ADVICE_API_KEY;
- recusa --fix (isso subiria código-fonte — use local).
- Reprodutibilidade: temperature=0 + a chave de cache fingerprint+provider+model tornam as re-execuções determinísticas.
Dependências: RF-028, RF-029; RF-010 (offline bloqueia remote). Componente: internal/advisor/*.go.
RF-031 — Correção sugerida (--fix) com verify-the-fix¶
| Campo | Conteúdo |
|---|---|
| ID | RF-031 |
| Nome | Correção sugerida (--fix) com verify-the-fix |
| Descrição | Com --advice-provider=local e --fix=suggest, o Quorum propõe um patch para um achado que precisa passar por um re-scan verify-the-fix: o patch é aplicado a uma cópia temporária, re-escaneado com o mesmo scanner e mantido apenas se o achado sumiu e o arquivo ainda faz parse. Ele nunca aplica automaticamente. |
| Prioridade | Opcional |
Fluxo (caminho de verificação em internal/advisor): para cada achado candidato o modelo local propõe um patch; o patch é aplicado a uma cópia temporária, o mesmo scanner é reexecutado e a sugestão é retida apenas se o achado-alvo desaparece e o arquivo faz parse. Logado como fixes: %d verified / %d proposed.
Entradas: --fix (off|suggest, default off), um provider local (RF-030).
Saídas: patch verificado anexado ao achado (model.Fix); métricas quorum_advice_fix{stage=proposed|verified} onde verified/proposed é a taxa verify-the-fix (RF-023).
Regras de negócio
- Nunca aplica automaticamente: o patch é uma sugestão anexada ao relatório; o operador decide.
- Gate verify-the-fix: um patch proposto só é exposto como verificado se o re-scan confirmar que o achado sumiu e o arquivo ainda faz parse.
- --fix requer local: é recusado com --advice-provider=remote (subiria código-fonte do arquivo).
- --fix=off (default) ⇒ nenhum patch proposto.
Dependências: RF-030. Alimenta RF-023. Componente: internal/advisor/verify.go.
Rastreabilidade: RF → componente de código¶
| RF | Arquivos-chave |
|---|---|
| RF-001, RF-020 | cmd/quorum/scan.go (runScan, emit) |
| RF-002 | cmd/quorum/scan.go (resolveTargetType) |
| RF-003, RF-022 | cmd/quorum/root.go, cmd/quorum/main.go |
| RF-004, RF-005, RF-006, RF-007 | internal/orchestrator/orchestrator.go |
| RF-008 | internal/adapter/*.go (12 adapters), internal/model/model.go, internal/severity/severity.go, internal/purl |
| RF-009, RF-010 | internal/alias/resolver.go, internal/alias/osv.go |
| RF-011 | internal/cache/store.go |
| RF-012, RF-013 | internal/crosswalk/crosswalk.go, internal/correlate/correlate.go, crosswalk/*.yaml, cmd/quorum/scan.go |
| RF-014 | internal/correlate/key.go, internal/correlate/correlate.go |
| RF-015 | internal/consensus/consensus.go |
| RF-016 | internal/report/{report,sarif,json,xml}.go |
| RF-017, RF-018 | internal/filter/filter.go |
| RF-019 | cmd/quorum/scan.go (worstSeverity, gate), cmd/quorum/main.go |
| RF-021 | cmd/quorum/scan.go (printSummary, logf) |
| RF-023 | cmd/quorum/scan.go (writeMetricsFile), internal/report/metrics.go |
| RF-024 | cmd/quorum/scan.go (logf, validação de --log-format) |
| RF-025 | internal/adapter/adapter.go (extraArgs, splitArgs) |
| RF-026 | internal/adapter/adapter.go (capWriter, maxOutputBytes), cmd/quorum/scan.go (checkTargetSize) |
| RF-027 | cmd/quorum/scan.go (validateTargetRef) |
| RF-028 | internal/enrich/enrich.go, knowledge/*.yaml, cmd/quorum/scan.go |
| RF-029 | internal/rag/*.go, knowledge/owasp/corpus.yaml, cmd/quorum/advise_index.go |
| RF-030 | internal/advisor/{advisor,client}.go, cmd/quorum/scan.go |
| RF-031 | internal/advisor/verify.go |
Itens fora de escopo (N/A)¶
O template corporativo prevê capacidades que não existem no Quorum por decisão arquitetural ("CLI/Docker only"). Estas são declaradas N/A com justificativa:
| Capacidade | Status | Justificativa técnica |
|---|---|---|
| Frontend web / dashboard | N/A | O produto é panel-less e CI/CD-first (root.go: "No panel, no daemon"). A saída é um relatório (SARIF/JSON/XML) + código de saída + métricas Prometheus (RF-023). |
| Banco de dados relacional | N/A | A persistência limita-se ao cache JSON de aliases (internal/cache) e ao cache de advice em disco — explicitamente "sem puxar um banco em CGO". |
| API REST / servidor HTTP | N/A | Não há servidor; as chamadas de rede de saída são OSV.dev para aliases (RF-009) e, apenas quando opt-in, o endpoint consultivo (RF-030). --metrics escreve um arquivo local, não expõe um endpoint. |
| Autenticação / contas de usuário | N/A | Sem multiusuário; a ferramenta roda no contexto de processo do CI/desenvolvedor. QUORUM_ADVICE_API_KEY (RF-030) é uma credencial de provider de saída, não auth de usuário. |
| IA / LLM no núcleo determinístico | N/A | Correlação/consenso é determinística (chaves + fórmula), sem modelos de ML. Uma camada consultiva opt-in (--advice, RF-028…RF-031) adiciona recomendações de IA, mas ela é desligada por padrão, apenas de apresentação, e nunca afeta correlação/confiança/gating. |
| Orquestração cloud/K8s em runtime | N/A | k8s é um tipo de alvo (manifests/cluster a escanear), não um runtime do próprio Quorum. |
Propostas futuras (claramente separadas do as-is): TTL/expiração no cache de aliases; um exportador de baseline (gerar
.quorumignorea partir de um relatório); um formato de saída adicional (ex. SARIF + resumo em Markdown); correlação de RBAC multi-engine (hoje single-engine, RF-012). Nada disso está implementado na v0.8.3. A camada consultiva (RF-028…RF-031), em contraste, está implementada e disponível na v0.8.3.
Links cruzados¶
- Modelo de dados, matriz de correlação (§6), matemática do consenso (§9) e status dos scanners (§14): ver
DESIGN.mdna raiz do repositório. - Documentos relacionados nesta suíte: 01-visao-geral · 03-requisitos-nao-funcionais · 04-arquitetura · 06-interfaces-cli-e-formatos · 13-ia · 21-proposta-ia.
- Documentação publicada (MkDocs Material) no GitHub Pages; avisos de terceiros em
THIRD_PARTY_NOTICES.md.
Premissas¶
- Versão de referência: o documento descreve o comportamento as-is da v0.8.3 (revisão 2026-07-04); a constante
versionemroot.goainda é0.1.0porque é sobrescrita em tempo de build (RF-022), o que assumimos ser intencional (GoReleaser/ldflags injetamv0.8.3). - Capacidades vs. disponibilidade:
list-scanners(RF-003) reporta as capacidades declaradas pelos 12 adapters, não a disponibilidade real do binário — a disponibilidade só é verificada durante oscan(RF-006). polaris/kube-scoreagora registrados: na v0.2.3,polarisexistia apenas emscannerCategorysem adapter. Na v0.8.3,polaris,kube-scoreekubescapesão adapters registrados que produzemK8S_POSTUREe são correlacionados pelo hub k8s do crosswalk (crosswalk/k8s.yaml).- Cobertura de alvo por adapter: a matriz do RF-008 reflete os métodos
Supports/Capabilitieslidos diretamente do código; divergências entreSupports(o que roda) eCapabilities(o que olist-scannersmostra) foram preservadas — ex.kubescapeepolaristêmSupportsrepo+k8s, mas declaram capacidade apenas paraK8S_POSTUREem k8s. conftesté opt-in: sem políticas Rego (default./policyouQUORUM_CONFTEST_ARGS="--policy ..."), o conftest erra e é reportado comoerror(RF-007/RF-008) — comportamento esperado de policy-as-code, não um defeito.- RBAC single-engine: a correlação de RBAC não é multi-engine porque a análise de RBAC do kubescape requer contexto de cluster (RF-012); é uma decisão de projeto documentada, não uma lacuna do crosswalk.
- Cache sem expiração:
internal/cachenão implementa TTL (versionado porschemaVersion); assume-se que o usuário gerencia/limpa o arquivo manualmente quando necessário (listado como proposta futura). Nota relacionada: o DB do grype é pré-cacheado comGRYPE_DB_VALIDATE_AGE=false(não expira) na supply chain da imagem — fora de escopo deste documento de RF. - Nomes entre documentos (
01-...,03-...,04-...) seguem a convenção numérica da suíte de docs, atualmente publicada no GitHub Pages. - OSV.dev como única dependência de rede sempre ativa: assume-se que nenhuma outra etapa do pipeline faz I/O de rede por padrão;
--offline(RF-010) basta para operação air-gapped. Os providers de IA da camada consultiva (RF-030) são chamadas de saída adicionais, opt-in —localfica on-host,remoteé egress-gated e bloqueado por--offline. --timeoutmapeia para um timeout por scanner (PerScannerTime), não para o scan inteiro;ProbeTime(60s) é separado e não exposto como flag de CLI na v0.8.3.- Caps com defaults altos:
QUORUM_MAX_OUTPUT_BYTES(512 MiB) eQUORUM_MAX_TARGET_BYTES(20 GiB) são guardas de DoS (RF-026) bem acima de qualquer uso real; assume-se que operadores só os ajustam em casos extremos. QUORUM_<SCANNER>_ARGSé confiável: o passthrough (RF-025) assume o mesmo nível de confiança que os flags de CLI (o operador controla o ambiente do container/CLI).- A camada consultiva é opt-in e não-bloqueante:
--advicee seus sub-flags (RF-028…RF-031) são desligados por padrão; quando habilitados, apenas adicionam apresentação, degradam graciosamente se um modelo/fonte de conhecimento estiver indisponível, e nunca podem alterarcorrelationKey/fingerprint/confidence/severidade agregada/o gate--fail-on. Sem--advice, a saída é byte-a-byte idêntica.