Ir para o conteúdo

Interfaces (CLI) e Formatos de Saída

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

O Quorum (quorum-sec-scan, v0.8.3) é uma ferramenta de consensus security scanning exclusivamente CLI/Docker. Não há frontend web, banco de dados relacional, API REST/HTTP nem camada de autenticação. Portanto, o equivalente a "APIs" neste produto é o conjunto de contratos de interface que ele expõe ao mundo externo:

  1. A interface de linha de comando (quorum scan, quorum list-scanners) — entradas (args/flags/env), saídas (stdout/stderr/arquivo) e exit codes;
  2. Os formatos de saída (SARIF primário, JSON, XML) — cada um um contrato de serialização estável — mais a telemetria Prometheus (--metrics) como formato auxiliar;
  3. A GitHub Action composta (action.yml) — inputs/outputs declarativos que envolvem a imagem assinada :full.

Este documento trata cada comando, flag e formato como um contrato versionado: método de invocação, entradas, saídas, validação, erros e exemplos. Onde o template de "API" pede OpenAPI/HTTP, registramos N/A com justificativa técnica e entregamos, no lugar, um JSON Schema da saída JSON e a interface declarativa do action.yml.

Referências de código (fonte de verdade desta página): cmd/quorum/scan.go, cmd/quorum/root.go, internal/report/, internal/orchestrator/orchestrator.go, internal/adapter/adapter.go, internal/model/model.go, action.yml.


1. Por que "API HTTP / OpenAPI = N/A"

Item do template de "API" Aplicabilidade no Quorum Justificativa técnica
Endpoint HTTP / REST N/A Não há servidor, daemon nem listener de rede. O binário executa, produz o relatório e sai. O root.go afirma explicitamente "No panel, no daemon".
Especificação OpenAPI/Swagger N/A Não há superfície HTTP a descrever. O contrato equivalente é a CLI (esta página) + o JSON Schema da saída (§7) + a interface do action.yml (§8).
Autenticação / OAuth / API keys N/A Sem contas, sem sessão, sem multi-tenancy. A única credencial relevante é a verificação cosign keyless (OIDC) da imagem na Action — não é autenticação de usuário. O passthrough de chaves de plataforma (ex.: --bc-api-key do Checkov) é feito via env, não por login. Ver 10-infraestrutura.md.
Rate limiting de API N/A para a CLI; aplica-se indiretamente ao OSV.dev A CLI não impõe nem sofre rate limit próprio. O único acesso de rede é a resolução de aliases via OSV.dev, com degradação graciosa em falha/limite e desligamento total via --offline. Ver §5 e 07-persistencia-e-artefatos.md.
Quotas / limites de recursos Aplica-se como caps anti-DoS via env Dois tetos protegem o processo: QUORUM_MAX_OUTPUT_BYTES (512 MiB de stdout bufferizado por scanner) e QUORUM_MAX_TARGET_BYTES (20 GiB de árvore em disco). Ver §4.3.
Versionamento de API Aplica-se como versionamento de release/semver + o schema quorum/v1 Ver §9.

A rede só é tocada para enriquecimento de aliases (OSV.dev) e, no contexto da Action, para baixar/verificar a imagem. O fluxo de scan em si é local ao host/contêiner.


2. Mapa de interfaces

flowchart LR
    subgraph Invocation
        A[Binário nativo<br/>quorum]
        B[Docker<br/>ghcr.io/.../quorum-sec-scan:full|:slim]
        C[GitHub Action<br/>Martinez1991/quorum-sec-scan@v0]
    end
    A --> CLI[CLI cobra]
    B --> CLI
    C -->|docker run| B
    CLI -->|scan <target>| ORCH[Orquestrador<br/>12 scanners]
    CLI -->|list-scanners| REG[Registro de adapters]
    ORCH --> REP[report.Write]
    REP -->|sarif / json / xml| OUT{--output?}
    OUT -->|vazio| STDOUT[stdout]
    OUT -->|arquivo| FILE[arquivo 0600 em disco]
    ORCH -.->|--metrics| MET[report.WriteMetrics<br/>Prometheus textfile 0644]
    CLI -->|progresso/resumo| STDERR[stderr text|json]
    CLI -->|0 / 1 / 2| EXIT[exit code]

3. Contrato global da CLI

Aspecto Contrato
Binário quorum
Comandos scan <target>, list-scanners, advise-index
Flags globais --version / -v (imprime a versão), --help / -h
Versão Injetada no build via -ldflags "-X main.version=..."; default de fallback 0.1.0 (root.go). A versão também é gravada no driver SARIF e no namespace do fingerprint (report.Version).
stdout Apenas o relatório quando --output está vazio. Nada mais é escrito no stdout.
stderr Logs de progresso e o bloco humano de resumo. Formato de log selecionável via --log-format text\|json. Silenciável com --quiet/-q.
Comportamento de erro SilenceUsage: true e SilenceErrors: true na raiz — os erros são tratados por main (sem usage dump barulhento).

3.1 Contrato de exit code (compartilhado por todos os comandos)

Exit code Significado Origem no código
0 OK — execução concluída e nenhum finding atingiu --fail-on (ou --fail-on ausente). Retorno normal de runScan.
1 Gate acionado — ao menos um finding tem severidade >= --fail-on. os.Exit(1) em runScan após severity.AtLeast(worst, failThreshold).
2 Erro de uso ou runtime — flag inválida, baseline ausente, formato desconhecido, --log-format inválido, target iniciando com -, target acima do cap de tamanho, falha ao carregar crosswalk, erro fatal de pipeline. Retorno error de RunE, convertido para exit 2 por main.

Princípio operacional: o exit code é o mecanismo de gating no CI. 0 não significa "seguro" — significa "nada cruzou o limiar". O próprio resumo reforça: "0 findings não é prova de segurança". Ver 09-backend.md.


4. Comando scan <target> — contrato detalhado

4.1 Invocação

quorum scan <target> [flags]
  • <target> é obrigatório e exatamente 1 argumento (cobra.ExactArgs(1)). Zero ou mais de um argumento → erro de uso (exit 2).
  • <target> é uma referência de imagem (alpine:3.19), um diretório de repositório/IaC (., /work) ou um diretório de manifests k8s.
  • Hardening (anti injeção de argumentos): um <target> iniciando com - é rejeitado (validateTargetRef), pois um scanner a jusante poderia interpretá-lo como flag. Para um caminho literal, use ./-nome (exit 2 com uma mensagem sugerindo a forma).

4.2 Entradas — flags

Definidas em cmd/quorum/scan.go (newScanCmd):

Flag Curta Tipo Default Descrição / validação
--type string "" (inferido) image \| repo \| k8s. Aceita aliases: repo = fs/dir, k8s = kubernetes/manifests. Valor inválido → erro (exit 2). Se omitido, infere: um caminho existente em disco ⇒ repo; caso contrário ⇒ image.
--scanners string (CSV) "" (todos) Lista separada por vírgula. Normalizada para minúsculas, espaços e itens vazios descartados. Nomes desconhecidos não abortam: emitem warning: unknown scanner ... e são ignorados (orquestrador).
--format -f string sarif sarif \| json \| xml (case-insensitive, trimmed). Inválido → unknown format ... (exit 2).
--output -o string "" (stdout) Caminho do arquivo de saída. O caminho é normalizado com filepath.Clean; diretórios-pai são criados (MkdirAll 0755); o arquivo é escrito com modo 0600 (apenas dono — o relatório pode carregar detalhe sensível de finding). Vazio ⇒ stdout.
--fail-on string "" critical \| high \| medium \| low. Habilita o gating. Inválido → erro (exit 2).
--min-severity string "" Remove findings abaixo deste nível do relatório e do gating (filtro aplicado antes de emitir/gating). Inválido → erro (exit 2).
--baseline string .quorumignore Arquivo de fingerprints/correlationKeys a suprimir. Se o usuário passou explicitamente a flag e o arquivo não existe ⇒ erro (exit 2). Se for o default e não existir ⇒ prossegue sem baseline.
--crosswalk string ./crosswalk Diretório de mapeamentos. Se default e ./crosswalk ausente, cai automaticamente para /opt/quorum/crosswalk (bundle da imagem). Se passado explicitamente, é honrado verbatim.
--advice bool false Anexa remediação determinística baseada em templates + referências OWASP aos findings, incluindo retrieval (RAG) a partir de um corpus OWASP versionado e pinado por digest (knowledge/owasp/, retrieval léxico determinístico, sem modelo). Camada consultiva, sem IA por padrão; apenas apresentação: não altera correlationKey/fingerprint/confidence/severidade nem o gating. Sem a flag, a saída é byte-idêntica. Ver 21-proposta-ia (Fases 0 e 2).
--knowledge string ./knowledge Diretório do knowledge pack (templates + refs OWASP) usado por --advice. Mesmo fallback do crosswalk: se default e ausente, cai para /opt/quorum/knowledge (bundle da imagem).
--advice-provider string none none \| local \| remote. Com local, consulta um LLM no host (endpoint compatível com OpenAI) por uma recomendação por finding. Com remote, consulta uma API externa (Fase 3) — envia findings para fora do host, então exige --advice-allow-egress + QUORUM_ADVICE_API_KEY, é bloqueado por --offline e recusa --fix (não faz upload de código). Apenas apresentação, rotulado "AI-generated, advisory only"; não afeta o gating. Degradação graciosa: modelo inacessível ⇒ sem advice, o scan não falha. local é no host (não desligado por --offline).
--advice-allow-egress bool false Consentimento explícito para enviar findings (títulos, caminhos, controles) a um serviço externo. Obrigatório para --advice-provider=remote; sem ele, remote é recusado.
--advice-endpoint string http://localhost:11434/v1 Base URL compatível com OpenAI (ex.: Ollama) para --advice-provider=local.
--advice-model string qwen2.5-coder:7b Id do modelo local. Compõe a chave de cache (reprodutibilidade).
--advice-embed-model string nomic-embed-text Modelo de embedding para retrieval semântico do corpus OWASP. Usado apenas quando o corpus traz vetores (embedding:) e --advice-provider=local; caso contrário o retrieval é léxico (sem modelo).
--advice-cache string ~/.cache/quorum/advice.json Cache de advice chaveado por fingerprint+model. Mesmo finding+model ⇒ mesma recomendação (servida do cache).
--advice-max int 50 Máximo de findings enviados ao modelo por execução (0 = sem cap). Limite de custo/latência.
--fix string off off \| suggest. Com suggest, o modelo propõe um patch que só é anexado se passar no verify-the-fix (re-scan do arquivo corrigido: o finding sumiu e o arquivo faz parse). Nunca aplica automaticamente. Escopo: IaC/K8s (MISCONFIG/K8S_POSTURE); SCA/CVE e imagem ficam de fora.
--cache string ~/.cache/quorum/aliases.json (via os.UserCacheDir) Arquivo de cache do resolvedor de aliases. Cai para .quorum-cache.json se o diretório de cache do SO não resolver.
--metrics string "" (off) Escreve métricas em text-format Prometheus neste arquivo, ao lado do relatório normal (telemetria). Caminho normalizado com filepath.Clean; diretórios-pai criados; arquivo escrito com modo 0644 (contagens não sensíveis, destinadas a scraping). Ver §7.5.
--log-format string text text \| json. Controla o formato dos logs de progresso no stderr. text[quorum] ...; json ⇒ uma linha JSON por evento ({"ts","level","msg"}). Inválido → invalid --log-format "x" (want text\|json) (exit 2).
--timeout duration 5m Timeout por scanner (não global). Formato time.Duration do Go (30s, 2m, 1h).
--offline bool false Desliga as consultas ao OSV.dev (usa os aliases locais do scanner + cache).
--quiet -q bool false Suprime logs de progresso e o resumo no stderr (independente de --log-format).

Nota sobre o probe de versão: o timeout do probe (Options.ProbeTime, default interno) é uma constante do orquestrador e não é exposto como flag nesta versão. Ele distingue timeout / killed(OOM) / não-instalado. Ver 09-backend.md.

4.3 Entradas — variáveis de ambiente

Diferentemente de versões anteriores, a CLI agora lê suas próprias variáveis de ambiente (não há binding env→flag para as flags acima, mas há env para passthrough e para os caps anti-DoS). Herdadas do ambiente / SO:

  • HOME / equivalentes — usadas por os.UserCacheDir() para resolver o default de --cache.

Passthrough por scanner (internal/adapter/adapter.go, extraArgs):

  • QUORUM_<SCANNER>_ARGS — argumentos CLI extras anexados à invocação daquele scanner, sem alterar o adapter. <SCANNER> é o nome do adapter em MAIÚSCULAS (ex.: QUORUM_TRIVY_ARGS, QUORUM_GRYPE_ARGS, QUORUM_CHECKOV_ARGS, QUORUM_KICS_ARGS, QUORUM_DOCKLE_ARGS, QUORUM_KUBESCAPE_ARGS). O valor é dividido em estilo shell (respeita aspas simples/duplas; sem expansão de variável). Uso típico: QUORUM_CHECKOV_ARGS="--bc-api-key <key> --repo-id org/repo" desbloqueia políticas Prisma Cloud/Bridgecrew através do Checkov OSS embarcado. É um controle de operador (mesmo nível de confiança das flags); valores podem carregar segredos e por isso não são ecoados.

Caps de proteção (DoS):

  • QUORUM_MAX_OUTPUT_BYTES — teto de stdout bufferizado por scanner (default 512 MiB). Se um scanner (ou uma zip/xml bomb) o exceder, a execução é abortada com erro claro em vez de causar OOM. internal/adapter/adapter.go.
  • QUORUM_MAX_TARGET_BYTES — teto para o tamanho em disco de um target repo/k8s (default 20 GiB; 0 desabilita). A varredura de tamanho para assim que o cap é cruzado (repos normais pagam apenas um stat leve). Targets image (sem árvore local) são ignorados. Valor inválido → erro (exit 2). cmd/quorum/scan.go (checkTargetSize).

A Action (action.yml) injeta os inputs como env dentro de seu próprio shell script, traduzindo-os em flags da CLI e em QUORUM_<SCANNER>_ARGS; isso é um detalhe da Action (§8), não da CLI.

4.4 Saídas

flowchart TD
    R[runScan] --> EMIT[emit]
    EMIT -->|output == ""| SO[cmd.OutOrStdout → stdout]
    EMIT -->|output != ""| WF[filepath.Clean<br/>os.WriteFile 0600<br/>cria diretórios-pai]
    R --> PS[printSummary → stderr]
    PS -. --quiet .-> NONE[suprimido]
    R -->|--metrics != ""| MW[writeMetricsFile 0644<br/>Prometheus textfile]
    R --> GATE{gating?}
    GATE -->|worst >= fail-on| EX1[os.Exit 1]
    GATE -->|caso contrário| EX0[return nil → 0]
  • stdout: o relatório serializado (SARIF/JSON/XML) quando --output está vazio.
  • arquivo (--output): o mesmo conteúdo, com modo 0600.
  • arquivo (--metrics): métricas Prometheus, com modo 0644 (§7.5).
  • stderr: logs de progresso (target=... type=... crosswalk=N rules ..., status por scanner, uma linha de filtragem quando há supressões) e o bloco ── quorum summary ── (contagens por severidade, multi-detected, elapsed, e a nota "0 findings não é prova de segurança"). No modo --log-format json, os logs de progresso saem como uma linha JSON por evento; o bloco humano de resumo permanece como texto. Tudo é suprimido por --quiet.

4.5 Validação e erros (resumo)

Condição Mensagem (forma) Exit
nº de args ≠ 1 erro de args do cobra 2
<target> inicia com - invalid target "-x": must not start with '-' (use "./-x" for a path) 2
--type inválido invalid --type "x" (want image\|repo\|k8s) 2
--log-format inválido invalid --log-format "x" (want text\|json) 2
Target acima do cap target "x" exceeds the N-byte size cap (...QUORUM_MAX_TARGET_BYTES) 2
QUORUM_MAX_TARGET_BYTES não numérico invalid QUORUM_MAX_TARGET_BYTES "x" 2
--fail-on inválido invalid --fail-on "x" (want critical\|high\|medium\|low) 2
--min-severity inválido invalid --min-severity "x" (...) 2
--advice-provider inválido invalid --advice-provider "x" (want none\|local\|remote) 2
remote sem --advice-allow-egress --advice-provider=remote sends your findings ...; re-run with --advice-allow-egress to consent ... 2
remote sob --offline --advice-provider=remote is disabled by --offline ... 2
remote sem API key --advice-provider=remote needs an API key in QUORUM_ADVICE_API_KEY 2
--fix com remote --fix is not allowed with --advice-provider=remote ... 2
--baseline explícito ausente baseline file not found: <path> 2
--format inválido unknown format "x" (want sarif\|json\|xml) 2
Falha ao carregar crosswalk loading crosswalk: ... 2
Falha ao escrever métricas writing metrics: ... 2
Erro de pipeline (orquestrador) erro propagado 2
Finding >= --fail-on (não é erro) gate logado, exit 1 1

4.6 Exemplos

# 1) Scan de repositório, gate em HIGH, SARIF para arquivo (caso típico de CI)
quorum scan . --type repo --fail-on high -o quorum.sarif

# 2) Scan de imagem, apenas dois scanners, saída JSON para stdout
quorum scan alpine:3.19 --type image --scanners trivy,grype --format json

# 3) Manifests k8s, offline, suprimindo findings abaixo de MEDIUM
quorum scan ./k8s --type k8s --offline --min-severity medium

# 4) Com baseline, timeout por scanner maior e métricas Prometheus em textfile
quorum scan . --baseline .quorumignore --timeout 10m \
  -o report.xml -f xml --metrics /var/lib/node_exporter/quorum.prom

# 5) Logs JSON (para agregadores) e passthrough de args para o Checkov
QUORUM_CHECKOV_ARGS="--bc-api-key $BC_KEY --repo-id org/repo" \
  quorum scan . --type repo --log-format json --fail-on high -o quorum.sarif

# 6) Camada consultiva com modelo local + sugestões verify-the-fix
quorum scan . --type repo --advice --advice-provider local \
  --advice-model qwen2.5-coder:7b --fix suggest -o quorum.sarif

# 7) Via Docker (imagem :full autocontida)
docker run --rm -v "$PWD:/work" -w /work \
  ghcr.io/martinez1991/quorum-sec-scan:full \
  scan . --type repo --fail-on critical -o quorum.sarif

5. --offline, OSV.dev e rate limiting

  • Sem --offline, o resolvedor de aliases pode consultar o OSV.dev (preferindo IDs CVE), com um cache local em ~/.cache/quorum/aliases.json (um arquivo com modo 0600 e um schemaVersion — ver 07-persistencia-e-artefatos.md).
  • A CLI não implementa rate limiting próprio e não expõe controles de throttling. Em falha de rede ou indisponibilidade do OSV, há degradação graciosa: o pipeline continua com os aliases locais do scanner + cache, sem abortar.
  • --offline desliga completamente o acesso de rede do resolvedor (osv passa a nil em runScan). Em ambientes de CI air-gapped, é a flag a usar.

Detalhes em 07-persistencia-e-artefatos.md.


6. Comando list-scanners — contrato

6.1 Invocação

quorum list-scanners
  • Sem argumentos, sem flags específicas.
  • Lista os adapters registrados (ordenados por nome) e os tipos de finding que suportam (Capabilities()).

6.2 Saída

  • stdout, uma linha por scanner, formato "%-12s %v" (nome alinhado + slice de tipos). Na v0.8.3 há 12 adapters registrados. A saída canônica (derivada das Capabilities() em internal/adapter/):
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]

Os 12 scanners cobrem quatro famílias: SCA/VULN (trivy, grype), MISCONFIG/IaC (checkov, kics, terrascan, tfsec, regula, conftest), K8S_POSTURE (kubescape, polaris, kube-score) e IMG_HARDENING (dockle). O conftest roda policy-as-code com seu próprio Rego a partir de ./policy. Ver 09-backend.md e os adapters em internal/adapter/.

6b. Comando advise-index — habilitando RAG semântico

O corpus OWASP (21-proposta-ia, Fase 2) roda léxico por padrão (sem vetores). Para habilitar retrieval semântico, embede o corpus uma vez contra um endpoint de embeddings local (ex.: Ollama):

quorum advise-index \
  --corpus knowledge/owasp/corpus.yaml \
  --advice-endpoint http://localhost:11434/v1 \
  --advice-embed-model nomic-embed-text
  • Adiciona embedding: a cada chunk e registra o embedModel no arquivo. O digest de conteúdo é preservado (embeddings são excluídos do hash), então o pin permanece válido.
  • A partir daí, scan --advice --advice-provider local escolhe o retriever semântico automaticamente (o scan embeda a query com o mesmo modelo registrado no corpus). Sem embeddings, permanece léxico e determinístico.

6.3 Exit codes

  • 0 em sucesso; 2 apenas em erro de runtime inesperado.

7. Contrato de formato de saída

Selecionado por --format/-f. Os três formatos de relatório serializam o mesmo orchestrator.Result (internal/report/report.goWrite). Diferem em forma e consumidor-alvo. Há também um formato auxiliar de telemetria (Prometheus), emitido em paralelo por --metrics (§7.5).

flowchart LR
    RES[orchestrator.Result] --> W{Formato}
    W -->|sarif| S[writeSARIF<br/>SARIF 2.1.0]
    W -->|json| J[writeJSON<br/>quorum JSON]
    W -->|xml| X[writeXML<br/>quorumReport]
    RES -.->|--metrics| M[WriteMetrics<br/>Prometheus text-format]

7.1 SARIF (primário) — --format sarif

Fonte: internal/report/sarif.go.

  • $schema: https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json
  • version: 2.1.0
  • Um único run com:
  • tool.driver: name: "quorum", informationUri: "https://github.com/quorum-sec/quorum", version (= report.Version), e a lista rules (deduplicada por ruleId, ordenada por id).
  • results[]: um por MergedFinding.
  • properties do run: target (ref) e scanners (array de {name, status, version}).

Contrato de cada result:

Campo Origem Nota
ruleId sarifRuleID(m) Para VULN: VulnID do 1º membro (CVE/GHSA). Para os demais tipos: CanonicalControl (AVD/CIS) ou, na falta, o RuleID do scanner; fallback final: CorrelationKey.
level sarifLevel(severity) CRITICAL/HIGHerror; MEDIUMwarning; demais ⇒ note.
message.text m.Title
locations[] membros com Location.File Deduplicado por arquivo; region.startLine/endLine quando StartLine > 0.
partialFingerprints { "quorum/v1": m.Fingerprint } Chave de correlação estável entre execuções (= sha256(correlationKey)). É o que evita duplicação/re-alert no GitHub Code Scanning.
properties Objeto Quorum detectedBy (lista de scanners), detectionCount, confidence (arredondado a 2 casas), severity, correlationKey, unmapped.

Exemplo (trecho):

{
  "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
  "version": "2.1.0",
  "runs": [
    {
      "tool": {
        "driver": {
          "name": "quorum",
          "informationUri": "https://github.com/quorum-sec/quorum",
          "version": "0.8.3",
          "rules": [
            { "id": "CVE-2023-1234", "name": "VULN",
              "shortDescription": { "text": "openssl: heap overflow" },
              "properties": { "type": "VULN" } }
          ]
        }
      },
      "results": [
        {
          "ruleId": "CVE-2023-1234",
          "level": "error",
          "message": { "text": "openssl: heap overflow" },
          "locations": [],
          "partialFingerprints": { "quorum/v1": "9f2b...c0" },
          "properties": {
            "detectedBy": ["trivy", "grype"],
            "detectionCount": 2,
            "confidence": 0.88,
            "severity": "HIGH",
            "correlationKey": "VULN|CVE-2023-1234|pkg:apk/alpine/openssl",
            "unmapped": false
          }
        }
      ],
      "properties": {
        "target": "alpine:3.19",
        "scanners": [
          { "name": "grype", "status": "ran", "version": "0.74.0" },
          { "name": "trivy", "status": "ran", "version": "0.50.0" }
        ]
      }
    }
  ]
}

O partialFingerprints["quorum/v1"] é o ponto de integração mais importante com plataformas que consomem SARIF (ex.: GitHub Advanced Security): garante deduplicação determinística baseada no consenso, não no scanner individual.

Redação de segredos: findings SECRET têm o trecho correspondente (o Match do trivy) redigido no adapter (redactSecretText) — apenas os primeiros 4 caracteres de cada token longo sobrevivem, seguidos de …REDACTED…. O relatório carrega o contexto do segredo sem vazar seu valor.

7.2 JSON — --format json

Fonte: internal/report/json.go. Encoder com indentação de 2 espaços e SetEscapeHTML(false).

Forma estável (jsonReport):

{
  "tool": "quorum",
  "version": "0.8.3",
  "target": { "type": "image", "ref": "alpine:3.19" },
  "scanners": [ /* []orchestrator.ScannerRun */ ],
  "summary": {
    "totalFindings": 12,
    "durationMs": 8421,
    "bySeverity": { "CRITICAL": 1, "HIGH": 4, "MEDIUM": 5, "LOW": 2 },
    "multiDetected": 6
  },
  "findings": [ /* []model.MergedFinding */ ]
}

7.2.1 JSON Schema (Draft 2020-12) — substituto de OpenAPI

Este é o contrato formal da saída JSON. Reflete jsonReport, ScannerRun e MergedFinding (com members = Finding). Campos com a tag Go omitempty são opcionais aqui.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://github.com/quorum-sec/quorum/schema/quorum-report-v1.json",
  "title": "Quorum JSON Report (quorum/v1)",
  "type": "object",
  "required": ["tool", "version", "target", "scanners", "summary", "findings"],
  "additionalProperties": false,
  "properties": {
    "tool":    { "const": "quorum" },
    "version": { "type": "string", "description": "binary/report version" },
    "target": {
      "type": "object",
      "required": ["type", "ref"],
      "properties": {
        "type": { "type": "string", "enum": ["image", "repo", "k8s"] },
        "ref":  { "type": "string" }
      },
      "additionalProperties": false
    },
    "scanners": {
      "type": "array",
      "items": { "$ref": "#/$defs/scannerRun" }
    },
    "summary": {
      "type": "object",
      "required": ["totalFindings", "durationMs", "bySeverity", "multiDetected"],
      "properties": {
        "totalFindings": { "type": "integer", "minimum": 0 },
        "durationMs":    { "type": "integer", "minimum": 0 },
        "bySeverity": {
          "type": "object",
          "additionalProperties": { "type": "integer", "minimum": 0 }
        },
        "multiDetected": { "type": "integer", "minimum": 0,
          "description": "findings with detectionCount > 1" }
      },
      "additionalProperties": false
    },
    "findings": {
      "type": "array",
      "items": { "$ref": "#/$defs/mergedFinding" }
    }
  },
  "$defs": {
    "severity": {
      "type": "string",
      "enum": ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO", "UNKNOWN"]
    },
    "findingType": {
      "type": "string",
      "enum": ["VULN", "MISCONFIG", "SECRET", "K8S_POSTURE", "IMG_HARDENING"]
    },
    "scannerRun": {
      "type": "object",
      "required": ["name", "status", "findings", "durationMs"],
      "properties": {
        "name":      { "type": "string" },
        "version":   { "type": "string" },
        "status":    { "type": "string",
          "enum": ["ran", "skipped", "unavailable", "error", "timeout"] },
        "findings":  { "type": "integer", "minimum": 0 },
        "durationMs":{ "type": "integer" },
        "error":     { "type": "string" }
      },
      "additionalProperties": false
    },
    "mergedFinding": {
      "type": "object",
      "required": ["correlationKey", "type", "title", "severity",
                   "detectedBy", "detectionCount", "confidence",
                   "members", "fingerprint"],
      "properties": {
        "correlationKey": { "type": "string" },
        "type":           { "$ref": "#/$defs/findingType" },
        "title":          { "type": "string" },
        "severity":       { "$ref": "#/$defs/severity" },
        "detectedBy":     { "type": "array", "items": { "type": "string" } },
        "detectionCount": { "type": "integer", "minimum": 1 },
        "confidence":     { "type": "number", "minimum": 0, "maximum": 1 },
        "unmapped":       { "type": "boolean" },
        "members":        { "type": "array", "items": { "$ref": "#/$defs/finding" } },
        "fingerprint":    { "type": "string",
          "description": "sha256(correlationKey)" }
      },
      "additionalProperties": false
    },
    "finding": {
      "type": "object",
      "required": ["type", "scanner", "severity", "title"],
      "properties": {
        "type":             { "$ref": "#/$defs/findingType" },
        "scanner":          { "type": "string" },
        "scannerVersion":   { "type": "string" },
        "vulnId":           { "type": "string" },
        "aliases":          { "type": "array", "items": { "type": "string" } },
        "purl":             { "type": "string",
          "description": "pkg:type/ns/name@version" },
        "ruleId":           { "type": "string" },
        "canonicalControl": { "type": "string" },
        "category":         { "type": "string" },
        "unmapped":         { "type": "boolean" },
        "resource": {
          "type": "object",
          "properties": {
            "kind":      { "type": "string" },
            "name":      { "type": "string" },
            "namespace": { "type": "string" },
            "address":   { "type": "string" }
          },
          "additionalProperties": false
        },
        "location": {
          "type": "object",
          "properties": {
            "file":       { "type": "string" },
            "startLine":  { "type": "integer" },
            "endLine":    { "type": "integer" },
            "imageLayer": { "type": "string" }
          },
          "additionalProperties": false
        },
        "severity":       { "$ref": "#/$defs/severity" },
        "cvss":           { "type": "number", "description": "0 = absent" },
        "correlationKey": { "type": "string" },
        "fingerprint":    { "type": "string" },
        "title":          { "type": "string" },
        "description":    { "type": "string" },
        "confirmed":      { "type": "boolean",
          "description": "confirmed by an authoritative source (NVD/OSV)" }
      },
      "additionalProperties": false
    }
  }
}

Nota de fidelidade: o campo Raw de Finding tem a tag json:"-" e nunca é serializado. Por isso não aparece no schema acima. Da mesma forma, Result.Findings (canônico bruto) tem a tag json:"-" no nível de Result, mas o reporter JSON publica os findings merged via MergedFinding.Members, então os objetos Finding aparecem aninhados. ScannerRun usa um MarshalJSON customizado que emite durationMs em milissegundos (consistente com summary.durationMs).

7.3 XML — --format xml

Fonte: internal/report/xml.go. Espelha a estrutura JSON para pipelines legacy/JUnit-like. Cabeçalho xml.Header, indentação de 2 espaços.

Forma (quorumReport):

<?xml version="1.0" encoding="UTF-8"?>
<quorumReport tool="quorum" version="0.8.3">
  <target type="image">alpine:3.19</target>
  <scanners>
    <scanner name="trivy" status="ran" version="0.50.0" findings="9"></scanner>
    <scanner name="grype" status="ran" version="0.74.0" findings="7"></scanner>
  </scanners>
  <findings>
    <finding type="VULN" severity="HIGH" detectionCount="2" confidence="0.88"
             fingerprint="9f2b...c0">
      <correlationKey>VULN|CVE-2023-1234|pkg:apk/alpine/openssl</correlationKey>
      <title>openssl: heap overflow</title>
      <detectedBy>
        <scanner>trivy</scanner>
        <scanner>grype</scanner>
      </detectedBy>
      <locations>
        <location file="Dockerfile" startLine="3" endLine="3"></location>
      </locations>
    </finding>
  </findings>
</quorumReport>

Atributos/elementos relevantes: unmapped só aparece quando true (omitempty); error em <scanner> só quando presente; localizações deduplicadas por arquivo.

7.4 Comparação de formatos de relatório

Característica SARIF JSON XML
Padrão Sim Não Não
Consumidor-alvo GitHub Code Scanning, IDEs, dashboards SARIF automação/scripts, diff programático pipelines legacy/JUnit-like
Fingerprint estável partialFingerprints["quorum/v1"] findings[].fingerprint atributo fingerprint
Resumo agregado via properties + rules bloco summary dedicado atributos em <scanner>
Status por scanner run.properties.scanners scanners[] completo <scanners>
Membros brutos (Finding) não (apenas consenso) sim (members[]) parcial (locations/detectedBy)

7.5 Prometheus (telemetria) — --metrics <file>

Fonte: internal/report/metrics.go (WriteMetrics). Não é selecionado por --format: é emitido em paralelo com o relatório, apenas quando --metrics aponta para um arquivo. Text-format Prometheus, destinado a um textfile collector do node_exporter ou a um Pushgateway — telemetria exportável para uma CLI que não tem processo de longa duração a ser scraped. Arquivo escrito com modo 0644.

Séries emitidas (todas gauge):

Métrica Labels Significado
quorum_scan_duration_seconds tempo total wall-clock do scan (s).
quorum_scanner_up scanner, status 1 se o scanner rodou; 0 para skipped/unavailable/error/timeout.
quorum_scanner_findings scanner findings brutos por scanner (pré-consenso).
quorum_scanner_duration_seconds scanner duração por scanner (s).
quorum_findings_after_consensus findings restantes após o merge de consenso.
quorum_findings_total severity findings de consenso por severidade (CRITICAL/HIGH/MEDIUM/LOW/INFO).
quorum_multi_detected findings corroborados por mais de um scanner.
quorum_advice_enriched kind (apenas com --advice) findings enriquecidos por tipo: remediation, references, recommendation.
quorum_advice_provider provider (apenas com IA) provedor de recomendação ativo (local/remote), valor 1.
quorum_advice_fix stage (apenas com IA) fixes por estágio verify-the-fix: proposed vs verified (a razão é a taxa de sucesso).

As séries quorum_advice_* só aparecem quando --advice está ligado; sem ele, a saída de métricas é inalterada. provider/fix só surgem com um provedor de IA (local/remote); com Fase 0/2 pura há apenas quorum_advice_enriched.

Exemplo (trecho):

# HELP quorum_scan_duration_seconds Total scan wall-clock time in seconds.
# TYPE quorum_scan_duration_seconds gauge
quorum_scan_duration_seconds 8.421
# TYPE quorum_scanner_up gauge
quorum_scanner_up{scanner="trivy",status="ran"} 1
quorum_scanner_up{scanner="grype",status="ran"} 1
# TYPE quorum_findings_total gauge
quorum_findings_total{severity="CRITICAL"} 1
quorum_findings_total{severity="HIGH"} 4
# TYPE quorum_multi_detected gauge
quorum_multi_detected 6

8. Interface da GitHub Action (action.yml) — o substituto do "contrato de API"

Fonte: action.yml. Tipo composite. Disponível a partir de v0.2.1+; pinada via a tag móvel v0 (auto-avançada a cada release semver por tag-major.yml). A Action envolve a imagem :full (autocontida) e, por padrão, verifica a assinatura cosign antes de rodar.

flowchart TD
    U[uses: Martinez1991/quorum-sec-scan@v0] --> V{verify == true?}
    V -->|sim| C[cosign verify IMAGE<br/>OIDC issuer + identity regexp]
    V -->|não| MNT
    C --> MNT[monta -v WORKDIR:/work<br/>+ docker.sock se type=image<br/>+ host-gateway se advice-provider=local]
    MNT --> R[docker run --rm ... IMAGE scan ...<br/>+ envs QUORUM_*_ARGS]
    R --> O1[output-file]
    R --> O2[exit-code]
    R --> EX[exit code propagado ao step]

8.1 Inputs

Input Obrigatório Default Mapeia para
target não . arg <target>
type não "" --type (se não vazio)
scanners não "" --scanners (se não vazio)
format não sarif --format
output não quorum.sarif --output (relativo ao working dir)
fail-on não "" --fail-on (se não vazio)
min-severity não "" --min-severity (se não vazio)
baseline não "" --baseline (se não vazio)
crosswalk não /opt/quorum/crosswalk --crosswalk
timeout não "" --timeout (se não vazio)
offline não "false" --offline (quando "true")
quiet não "false" --quiet (quando "true")
image não ghcr.io/martinez1991/quorum-sec-scan:full imagem a rodar (pinning por @sha256:... recomendado em produção)
verify não "true" habilita cosign verify antes da execução
working-directory não ${{ github.workspace }} montado como /work no contêiner
docker-socket não "" Monta /var/run/docker.sock no contêiner. Auto-montado para type: image (o scanner precisa do daemon do runner para ver uma imagem local recém-buildada). "true" força para outros tipos; "off" desabilita mesmo em scan de imagem.
advice não "false" --advice (quando "true") — anexa a camada consultiva (templates da Fase 0 + RAG léxico da Fase 2).
advice-provider não none --advice-provider (none\|local\|remote). Para local, a Action adiciona automaticamente o mapeamento host-gateway para o contêiner alcançar um modelo no host do runner.
advice-endpoint não http://host.docker.internal:11434/v1 --advice-endpoint (base URL compatível com OpenAI; o default aponta para o host do runner via host.docker.internal).
advice-model não "" --advice-model (se não vazio)
advice-embed-model não "" --advice-embed-model (se não vazio) — retrieval OWASP semântico, apenas quando o corpus traz vetores.
advice-max não "" --advice-max (se não vazio)
advice-cache não "" --advice-cache (relativo ao working dir; persista com actions/cache para reprodutibilidade).
advice-allow-egress não "false" --advice-allow-egress (quando "true") — obrigatório para advice-provider=remote.
advice-api-key não "" encaminhado como env QUORUM_ADVICE_API_KEY para advice-provider=remote; nunca logado, bloqueado sob offline.
fix não "" --fix (off\|suggest) — requer advice-provider=local (remote recusa --fix).
trivy-args não "" env QUORUM_TRIVY_ARGS
grype-args não "" env QUORUM_GRYPE_ARGS
checkov-args não "" env QUORUM_CHECKOV_ARGS (ex.: --bc-api-key <key> desbloqueia políticas Prisma/Bridgecrew)
kics-args não "" env QUORUM_KICS_ARGS
dockle-args não "" env QUORUM_DOCKLE_ARGS
kubescape-args não "" env QUORUM_KUBESCAPE_ARGS

Os inputs consultivos são apenas apresentação e nunca tocam o gating: sem advice: true a saída é byte-idêntica. Os seis inputs *-args são os envs de passthrough explicitamente expostos pela Action. No nível CLI/Docker, o mecanismo QUORUM_<SCANNER>_ARGS funciona para qualquer scanner (§4.3); para os demais, injete o env diretamente via docker run -e.

8.2 Outputs

Output Descrição Origem
output-file Caminho absoluto do relatório escrito (${WORKDIR}/${OUTPUT}); vazio se a saída foi para stdout. steps.run.outputs.output-file
exit-code Exit code do Quorum: 0 ok, 1 gate, 2 erro. steps.run.outputs.exit-code

O step propaga o exit code de docker run (exit "${code}"), então o gating de --fail-on falha o job naturalmente. Para capturar o relatório sem falhar o job, combine com continue-on-error e leia o output exit-code.

8.3 Verificação de assinatura (cosign)

Quando verify: true, o step instala o cosign (se ausente) e executa:

cosign verify "${IMAGE}" \
  --certificate-identity-regexp \
    "https://github.com/Martinez1991/quorum-sec-scan/.github/workflows/release.yml@.*" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

Isso valida a assinatura OIDC keyless emitida pelo workflow de release. A cadeia de suprimentos é complementada em tempo de release por uma atestação de SLSA build-provenance e um SBOM SPDX atestado (via actions/attest-sbom) para a imagem e por-binário, além do sbom: true do BuildKit. O knowledge pack + crosswalk também recebem sua própria atestação de SLSA build-provenance a cada release (o job knowledge em release.yml); verifique-a com gh attestation verify knowledge/owasp/corpus.yaml. Ver 10-infraestrutura.md.

8.4 Montagem do socket Docker (evitando falso-zero)

Para type: image, um scan de uma imagem buildada localmente no runner é invisível de dentro do contêiner sem acesso ao daemon Docker do host. A Action, por padrão, auto-monta /var/run/docker.sock nesse caso, evitando o perigoso e silencioso "0 findings". Um scan de uma imagem de registry simplesmente ignora o socket. Comportamento:

  • docker-socket: "" (default): monta para type: image, não monta para os demais.
  • docker-socket: "true": força a montagem para qualquer tipo.
  • docker-socket: "off": desabilita mesmo em scan de imagem.
  • Se o socket não existe no runner, a Action emite um warning explicando o risco.

8.5 Exemplo de uso

name: security
on: [pull_request]
permissions:
  contents: read
  security-events: write   # para upload-sarif
jobs:
  quorum:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - id: scan
        uses: Martinez1991/quorum-sec-scan@v0
        with:
          target: .
          type: repo
          fail-on: high
          output: quorum.sarif
          advice: "true"          # remediação determinística + refs OWASP (apenas apresentação)
          checkov-args: "--bc-api-key ${{ secrets.PRISMA_KEY }} --repo-id org/repo"
        continue-on-error: true   # captura o SARIF mesmo se o gate falhar
      - name: Upload SARIF
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: ${{ steps.scan.outputs.output-file }}
      - name: Enforce gate
        if: steps.scan.outputs.exit-code == '1'
        run: exit 1

8.6 Checklist de adoção em CI

  • [ ] Pin da Action por tag (@v0) ou SHA de commit.
  • [ ] Em produção, pin da imagem por digest (image: ...@sha256:...).
  • [ ] verify: true (default) habilitado.
  • [ ] permissions: security-events: write se for fazer upload do SARIF.
  • [ ] fail-on definido conforme a política de gate do time.
  • [ ] offline: true em runners air-gapped (também bloqueia advice-provider=remote).
  • [ ] docker-socket deixado no default (auto) para scans de imagem local; "off" se a imagem vier apenas de um registry e o socket não deve ser exposto.
  • [ ] Segredos de plataforma passados via *-args (e advice-api-key) a partir de secrets.* (nunca hardcoded).
  • [ ] Captura de output-file para artifact/integração a jusante.

9. Versionamento e estabilidade de contrato

Contrato Como é versionado Estabilidade
Versão do produto SemVer por release (tags v[0-9]+.[0-9]+.[0-9]+). Gravada em version (build via ldflags). v0.x — pré-1.0; mudanças incompatíveis podem ocorrer em um minor, anunciadas no release.
Schema de saída (todos os formatos) Namespace quorum/v1 — visível como partialFingerprints["quorum/v1"] no SARIF e como $id .../quorum-report-v1.json no JSON Schema. A forma v1 é o contrato estável de findings; uma quebra incompatível introduziria quorum/v2.
CLI (flags/exit codes) Segue o SemVer do produto. Os exit codes 0/1/2 são um contrato de longo prazo. Estável; novas flags são aditivas (ex.: --metrics, --log-format, a família --advice*/--fix).
Passthrough / caps (env) QUORUM_<SCANNER>_ARGS, QUORUM_MAX_OUTPUT_BYTES, QUORUM_MAX_TARGET_BYTES, QUORUM_ADVICE_API_KEY. Aditivo; nomes estáveis. Valores são controles de operador.
Métricas Prometheus Nomes de série quorum_*. Aditivo; nomes tratados como contrato de telemetria.
Action (inputs/outputs) Tag móvel v0 para pinning (avançada por tag-major.yml); inputs aditivos. Estável dentro de v0.
Versão de scanner Reportada por scanner em scanners[].version; o probe distingue indisponibilidade. Informativo.

Compatibilidade de fingerprint: como Fingerprint = sha256(correlationKey) e o correlationKey é determinístico por tipo, qualquer mudança na construção da chave altera os fingerprints e é, portanto, tratada como quebra do contrato quorum/v1. Ver DESIGN.md §6 (matriz de correlação).


10. Rastreabilidade — flag → código → saída

flowchart LR
    F1[--fail-on] --> RS[runScan: gating]
    RS --> EC[exit 1]
    F2[--min-severity] --> FA[filter.Apply]
    F3[--baseline] --> LB[filter.LoadBaseline]
    LB --> FA
    F4[--format] --> PF[report.ParseFormat] --> WR[report.Write]
    F5[--output] --> EM[emit → stdout/arquivo 0600]
    F6[--scanners] --> SS[splitScanners] --> ORCH[orchestrator.Run]
    F7[--timeout] --> ORCH
    F8[--offline] --> AL[resolvedor de aliases / OSV]
    F9[--crosswalk] --> CW[crosswalk.Load + fallback]
    F10[--metrics] --> MW[writeMetricsFile 0644]
    F11[--log-format] --> LG[logf text|json]
    F12[--advice / --advice-provider / --fix] --> ADV[enrich / rag / advisor]
    ADV --> WR
    ENV[QUORUM_*_ARGS] --> ADP[adapter.extraArgs] --> ORCH
    ORCH --> MG[consensus.Merge] --> WR
    WR --> EM
    ORCH --> MW

Premissas

  1. Versão do produto (v0.8.3): o default de version no código é 0.1.0 (fallback de build); assumimos que o release v0.8.3 injeta 0.8.3 via -ldflags. Os exemplos usam "version": "0.8.3" para refletir o release documentado, não o default do fonte.
  2. Saída de list-scanners: a lista de 12 scanners e seus tipos é enumerada a partir do código (as Capabilities() de cada adapter em internal/adapter/), com o formato "%-12s %v". A ordenação é alfabética por nome (sort.Strings). Se um adapter mudar suas capacidades, a linha correspondente muda com ele.
  3. JSON Schema: escrito como uma representação fiel de jsonReport/ScannerRun/ MergedFinding/Finding (tags json), com additionalProperties: false como escolha editorial de rigor; o produto não publica este schema como arquivo no repositório nesta versão — é um artefato de documentação derivado do código.
  4. durationMs no JSON: na v0.8.3 é consistente. ScannerRun.MarshalJSON emite durationMs em milissegundos (s.Duration.Milliseconds()), consistente com summary.durationMs. Isso foi corrigido na linha v0.2.4 (#18); até a v0.2.3, scanners[].durationMs serializava em nanossegundos. ⚠️ Consumidores muito antigos (pré-v0.2.4) que leem o valor em ns devem ajustar — o contrato atual é ms.
  5. Variáveis de ambiente: diferentemente da premissa da v0.2.3, a CLI lê seu próprio env na v0.8.3 — QUORUM_<SCANNER>_ARGS (passthrough), QUORUM_MAX_OUTPUT_BYTES, QUORUM_MAX_TARGET_BYTES (caps anti-DoS) e QUORUM_ADVICE_API_KEY (chave do provedor remote, encaminhada pela Action). Não há, contudo, binding env→flag para as flags do scan (as flags só são definidas na linha de comando ou pelo shell da Action). O uso de os.UserCacheDir (indireto, via HOME) permanece.
  6. Passthrough para todos os scanners: extraArgs deriva o nome do env de QUORUM_<NOME em MAIÚSCULAS>_ARGS. Para adapters cujo nome não forma um identificador de env válido (ex.: kube-score → hífen), o env deve ser injetado equivalentemente pelo ambiente/host; a Action expõe apenas 6 inputs *-args. Não enumeramos aqui o env de cada um dos 12 scanners — o mecanismo é uniforme.
  7. A camada consultiva é opt-in e apenas apresentação: todas as flags --advice*/--fix e seus inputs de Action têm default desligado; sem eles a saída (e as métricas) são byte-idênticas ao núcleo determinístico, que não tem IA. Ver 21-proposta-ia e 13-ia.md.
  8. Cross-links: os arquivos 07-persistencia-e-artefatos.md, 09-backend.md e 10-infraestrutura.md são referenciados por convenção de numeração; podem ainda não existir no momento desta escrita.