Ir para o conteúdo

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 --metrics e --log-format; passthrough de argumentos por scanner via QUORUM_<SCANNER>_ARGS; caps de DoS para a saída dos scanners e o tamanho do alvo; validação do target contra argument injection; e hardening de I/O (relatório com 0600 + 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 em correlationKey, fingerprint, confidence, severidade agregada ou no gate --fail-on; sem --advice a 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 subcomando advise-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/orchestratorinternal/adapter (12 scanners) → internal/{alias,cache,crosswalk} (enriquecimento) → internal/correlateinternal/consensusinternal/filterinternal/{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-formattext|json, --advice-providernone|local|remote, --fixoff|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 ⇒ usa defaultProbeTime (60s).
  • Um scanner unavailable/skipped nã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-0086AVD-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/trivy já 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):

confidence = clamp01( 0.35·count + 0.25·diversity + 0.25·severity + 0.15·authoritative )
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 (writeMetricsFilereport.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. extraArgsQUORUM_<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 .quorumignore a 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.



Premissas

  1. Versão de referência: o documento descreve o comportamento as-is da v0.8.3 (revisão 2026-07-04); a constante version em root.go ainda é 0.1.0 porque é sobrescrita em tempo de build (RF-022), o que assumimos ser intencional (GoReleaser/ldflags injetam v0.8.3).
  2. 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 o scan (RF-006).
  3. polaris/kube-score agora registrados: na v0.2.3, polaris existia apenas em scannerCategory sem adapter. Na v0.8.3, polaris, kube-score e kubescape são adapters registrados que produzem K8S_POSTURE e são correlacionados pelo hub k8s do crosswalk (crosswalk/k8s.yaml).
  4. Cobertura de alvo por adapter: a matriz do RF-008 reflete os métodos Supports/Capabilities lidos diretamente do código; divergências entre Supports (o que roda) e Capabilities (o que o list-scanners mostra) foram preservadas — ex. kubescape e polaris têm Supports repo+k8s, mas declaram capacidade apenas para K8S_POSTURE em k8s.
  5. conftest é opt-in: sem políticas Rego (default ./policy ou QUORUM_CONFTEST_ARGS="--policy ..."), o conftest erra e é reportado como error (RF-007/RF-008) — comportamento esperado de policy-as-code, não um defeito.
  6. 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.
  7. Cache sem expiração: internal/cache não implementa TTL (versionado por schemaVersion); 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 com GRYPE_DB_VALIDATE_AGE=false (não expira) na supply chain da imagem — fora de escopo deste documento de RF.
  8. Nomes entre documentos (01-..., 03-..., 04-...) seguem a convenção numérica da suíte de docs, atualmente publicada no GitHub Pages.
  9. 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-inlocal fica on-host, remote é egress-gated e bloqueado por --offline.
  10. --timeout mapeia 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.
  11. Caps com defaults altos: QUORUM_MAX_OUTPUT_BYTES (512 MiB) e QUORUM_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.
  12. 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).
  13. A camada consultiva é opt-in e não-bloqueante: --advice e 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 alterar correlationKey/fingerprint/confidence/severidade agregada/o gate --fail-on. Sem --advice, a saída é byte-a-byte idêntica.