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:
- A interface de linha de comando (
quorum scan,quorum list-scanners) — entradas (args/flags/env), saídas (stdout/stderr/arquivo) e exit codes; - 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; - 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.
0nã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¶
<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 poros.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 targetrepo/k8s(default 20 GiB;0desabilita). A varredura de tamanho para assim que o cap é cruzado (repos normais pagam apenas um stat leve). Targetsimage(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
--outputestá vazio. - arquivo (
--output): o mesmo conteúdo, com modo0600. - arquivo (
--metrics): métricas Prometheus, com modo0644(§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 modo0600e umschemaVersion— 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.
--offlinedesliga completamente o acesso de rede do resolvedor (osvpassa anilemrunScan). 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¶
- 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 dasCapabilities()eminternal/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
conftestroda policy-as-code com seu próprio Rego a partir de./policy. Ver 09-backend.md e os adapters eminternal/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 oembedModelno 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 localescolhe o retriever semântico automaticamente (oscanembeda a query com o mesmo modelo registrado no corpus). Sem embeddings, permanece léxico e determinístico.
6.3 Exit codes¶
0em sucesso;2apenas 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.go → Write). 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.jsonversion:2.1.0- Um único
runcom: tool.driver:name: "quorum",informationUri: "https://github.com/quorum-sec/quorum",version(=report.Version), e a listarules(deduplicada porruleId, ordenada por id).results[]: um porMergedFinding.propertiesdo run:target(ref) escanners(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/HIGH ⇒ error; MEDIUM ⇒ warning; 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
SECRETtêm o trecho correspondente (oMatchdo 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
RawdeFindingtem a tagjson:"-"e nunca é serializado. Por isso não aparece no schema acima. Da mesma forma,Result.Findings(canônico bruto) tem a tagjson:"-"no nível deResult, mas o reporter JSON publica os findings merged viaMergedFinding.Members, então os objetosFindingaparecem aninhados.ScannerRunusa umMarshalJSONcustomizado que emitedurationMsem milissegundos (consistente comsummary.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--adviceestá ligado; sem ele, a saída de métricas é inalterada.provider/fixsó surgem com um provedor de IA (local/remote); com Fase 0/2 pura há apenasquorum_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: truea saída é byte-idêntica. Os seis inputs*-argssão os envs de passthrough explicitamente expostos pela Action. No nível CLI/Docker, o mecanismoQUORUM_<SCANNER>_ARGSfunciona para qualquer scanner (§4.3); para os demais, injete o env diretamente viadocker 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-onfalha o job naturalmente. Para capturar o relatório sem falhar o job, combine comcontinue-on-errore leia o outputexit-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 paratype: 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: writese for fazer upload do SARIF. - [ ]
fail-ondefinido conforme a política de gate do time. - [ ]
offline: trueem runners air-gapped (também bloqueiaadvice-provider=remote). - [ ]
docker-socketdeixado 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(eadvice-api-key) a partir desecrets.*(nunca hardcoded). - [ ] Captura de
output-filepara 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 ocorrelationKeyé determinístico por tipo, qualquer mudança na construção da chave altera os fingerprints e é, portanto, tratada como quebra do contratoquorum/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¶
- Versão do produto (v0.8.3): o default de
versionno código é0.1.0(fallback de build); assumimos que o release v0.8.3 injeta0.8.3via-ldflags. Os exemplos usam"version": "0.8.3"para refletir o release documentado, não o default do fonte. - Saída de
list-scanners: a lista de 12 scanners e seus tipos é enumerada a partir do código (asCapabilities()de cada adapter eminternal/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. - JSON Schema: escrito como uma representação fiel de
jsonReport/ScannerRun/MergedFinding/Finding(tagsjson), comadditionalProperties: falsecomo 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. durationMsno JSON: na v0.8.3 é consistente.ScannerRun.MarshalJSONemitedurationMsem milissegundos (s.Duration.Milliseconds()), consistente comsummary.durationMs. Isso foi corrigido na linha v0.2.4 (#18); até a v0.2.3,scanners[].durationMsserializava em nanossegundos. ⚠️ Consumidores muito antigos (pré-v0.2.4) que leem o valor em ns devem ajustar — o contrato atual é ms.- 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) eQUORUM_ADVICE_API_KEY(chave do provedor remote, encaminhada pela Action). Não há, contudo, binding env→flag para as flags doscan(as flags só são definidas na linha de comando ou pelo shell da Action). O uso deos.UserCacheDir(indireto, viaHOME) permanece. - Passthrough para todos os scanners:
extraArgsderiva o nome do env deQUORUM_<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. - A camada consultiva é opt-in e apenas apresentação: todas as flags
--advice*/--fixe 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. - Cross-links: os arquivos
07-persistencia-e-artefatos.md,09-backend.mde10-infraestrutura.mdsão referenciados por convenção de numeração; podem ainda não existir no momento desta escrita.