AUR-549 — SBOM CycloneDX 1.6 gerado com Trivy
Status
Implementado em internal/sbom (gerador, validador, pipeline, configuração)
e na wiring cmd/aurumcode/aur549.go (subcomando aurumcode sbom, aditivo
a review/fix). Prova de comportamento: cmd/aurumcode/aur549_test.go::
TestAUR549*; aceite: tests/acceptance/AUR-549.sh.
Problema
A adoção corporativa (RFC do dono, fora deste repositório público) pede um SBOM OWASP CycloneDX gerado com Trivy para o repositório e, quando houver, para uma imagem de container — nunca um SBOM vazio ou de outro formato/versão aceito silenciosamente. Esta é a base que o AUR-550 (envio ao Dependency-Track) consome; este card cobre só a geração e a validação.
v3: spec_version é um MÍNIMO, não um valor exato
A v2 (33253a0) validava specVersion por igualdade exata contra o
configurado ("1.6"). Na revisão, o Trivy fixado por digest em
.board/bootstrap/locks/scanners.yml (vuln_scanner_image, 0.73.0) emite
CycloneDX 1.7, e não existe flag do Trivy para pedir uma versão de
especificação mais antiga (Trivy CHANGELOG da 0.71.0, PR #10715) — ou seja,
a igualdade exata reprovava TODO SBOM real que esse binário gerasse, não
só uma entrada malformada.
A correção (internal/sbom/validator.go, specVersionAtLeast): aceitar
quando a saída tem a MESMA major do configurado e minor ≥ ao configurado.
Uma major diferente ("2.0" contra um configurado "1.6") continua recusada
mesmo sendo numericamente "maior" — mudança de major é incompatibilidade,
não avanço. Ambos os lados (a saída do Trivy e o spec_version
configurado) são exigidos no formato estrito major.minor;
internal/config.SBOMGeneratorConfig.Validate já recusa um spec_version
fora desse formato na CARGA da configuração, antes de qualquer chamada ao
Trivy — uma falha de "o que isso quer dizer" nunca vira uma falha de "o
Trivy devolveu algo estranho" mais tarde.
Downstream: o OWASP Dependency-Track só ingere CycloneDX 1.7 a partir da versão 5.1.0 do servidor (ou do backport 4.14.4) — o AUR-550 (ou quem configurar o servidor) precisa dessa versão mínima.
Decisão de arquitetura (v2): quality_gates dentro de internal/config
A v1 deste card não listava internal/config em seus paths e por isso
lia sua própria seção de um arquivo separado, .aurumcode/quality_gates.yml.
A v2 do card corrigiu esse defeito: internal/config passou a constar nos
paths, e a diretriz do dono é uma só configuração simples — um
.aurumcode/config.yml (mais Markdown) — nunca um segundo arquivo yml.
quality_gates agora é um campo de internal/config.Config
(QualityGates QualityGatesConfig, qualitygates.go), lido do MESMO
.aurumcode/config.yml que review/rules/ignore/gate/exceptions
já usam. QualityGatesConfig é a seção COMPARTILHADA por três cards de
adoção corporativa — Sast *SastConfig (AUR-548), SsorDtrack
*SsorDtrackConfig (este card, campo SBOMGenerator, mais o AUR-550) e
SupplyChain *SupplyChainConfig (reservado) — cada um um PONTEIRO: nil
significa "nunca declarado", distinto de "declarado, mesmo vazio". Isso é
o que permite a ApplyCentralPolicy (central.go) decidir a precedência
POR SEÇÃO: diferente de Gate/Exceptions (que uma política sempre
decide por completo, declarados ou não), aqui uma política que só
declara sast não arrasta ssor_dtrack com ela — o repositório continua
livre para configurar ssor_dtrack.sbom_generator por conta própria,
porque a política nunca opinou sobre essa chave especificamente. Só
quando a política declara ssor_dtrack (mesmo vazio) é que ela vale
sozinha, com a seção do repositório descartada e um aviso
(internal/config/central_test.go/qualitygates_test.go prova os três
casos: política muda, política não fala do assunto, política fala de
outra seção).
Como Config agora TEM o campo quality_gates, o decode estrito de
LoadCentralPolicy (yaml.Decoder.KnownFields(true)) reconhece a chave
normalmente — nenhuma política que declare quality_gates quebra mais a
decodificação, ao contrário da v1.
aurumcode sbom lê Gate E quality_gates.ssor_dtrack.sbom_generator numa
ÚNICA resolução efetiva (loadEffectiveConfig, cmd/aurumcode/aur549.go:
config.Load e, com --politica, ValidatePolicyOutsideReviewedTree +
LoadCentralPolicy + ApplyCentralPolicy) — a mesma sequência que
runReview/runPRReview já usam, agora também compartilhando a mesma
leitura de arquivo.
Comportamento
aurumcode sbom [--repo dir] [--imagem referencia] [--politica dir]
[--trivy-bin caminho]:
- Resolve o
Configefetivo (loadEffectiveConfig) e, dentro dele,QualityGates.SsorDtrack.SBOMGenerator. Sem seção declarada (nem no repositório, nem na política quando há uma): não faz nada, sai 0 (Documentation and compatibility do card). - Valida a seção (
SBOMGeneratorConfig.Validate,internal/config/ qualitygates.go):toolsó aceitatrivy,formatsó aceitacyclonedx; qualquer outro valor é erro de configuração (exit 2), antes de qualquer chamada externa. - Resolve
output_filecontra o repositório, recusando qualquer caminho que escape dele — absoluto,.., OU um link simbólico que resolva para fora da raiz (sbom.ResolveOutputPath, que rodaEvalSymlinkssobre o prefixo já existente do caminho). - Roda
trivy fs --format cyclonedx --output <tmp> <repo>(sbom.TrivyGenerator.GenerateFilesystem) para um arquivo temporário no mesmo diretório do destino, valida esse arquivo (sbom.Validate: JSON,bomFormat=CycloneDX,specVersionigual ao configurado) e só então o renomeia paraoutput_file(sbom.GenerateAndValidate) — AC-001. Uma falha em qualquer etapa (trivy ausente/erro, saída vazia, saída que não valida) nunca deixa um arquivo no caminho final — AC-002, AC-003. - Com
--imagem/--image, repete o mesmo pipeline comtrivy image --format cyclonedx --output <tmp> <imagem>, escrevendo em um segundo arquivo derivado deterministicamente (sbom.ImageOutputPath:<nome>-image<ext>) — AC-004. - Qualquer falha de geração/validação é reportada e roteada pelo MESMO gate
AUR-519 que a revisão usa (
evaluateGate,cmd/aurumcode/policygate.go), com o motivo própriosbom_generation_failure(gateReasonSBOMFailure):gate.inconclusive: block, ou nenhum gate declarado (este comando é novo — não há comportamento legado a preservar, e o Outcome do card exige "nunca SBOM vazio aceito") →exitQualityNotReviewed;gate.inconclusive: warn→ publica o motivo em stderr e sai 0, nunca bloqueando.
Testes
cmd/aurumcode/aur549_test.go usa um executável trivy falso (um script
sh) num diretório temporário colocado em PATH — nunca a rede, nunca o
binário real:
TestAUR549TrivyGeneratesValidatedSBOM(AC-001): trivy falso que grava CycloneDX 1.6 válido produz o arquivo configurado, com oargv(fs,--format cyclonedx,--output, o repositório) registrado e verificado.TestAUR549NonCycloneDXOutputRejected(AC-002): quatro subtestes —specVersionerrado,bomFormaterrado, saída que não é JSON, saída vazia — todos recusados, com o arquivo final confirmado ausente em cada caso.TestAUR549MissingTrivyIsInconclusive(AC-003): trivy ausente doPATHsob nenhum gate/block(falha fechado) e sobwarn(publica o motivo, sai 0).TestAUR549ImageProducesSeparateFile(AC-004): com--imagem, duas invocações de trivy (fseimage) produzem dois arquivos distintos.
tests/acceptance/AUR-549.sh roda os quatro testes acima dentro de uma
árvore semeada (go.mod go.sum cmd internal pkg) sem rede, e aplica
AC-002-MUT-001: substitui a única linha de decisão em
internal/sbom/validator.go (formatAndVersionOK, a comparação de
bomFormat/specVersion) por return true e confirma que o subteste
badversion de AC-002 vira RED — provando que o teste realmente depende da
validação, e não passaria mesmo aceitando qualquer saída.
Trivy como processo externo
internal/sbom.TrivyGenerator nunca embute um binário: resolve trivy via
PATH, ou via --trivy-bin apontando a outro executável (usado pelos
testes para apontar ao script falso, e, em CI, para o wrapper abaixo).
.github/workflows/review.yml ganhou a etapa "Generate SBOM (Trivy,
AUR-549)", que roda SEMPRE (revisão desta rodada: um grep de texto que só
decidia a mesma coisa que aurumcode sbom já decide, com o risco real de
discordar dela, foi removido) — o custo de rodar sem nada configurado é só
extrair o binário e gerar o wrapper; o docker run do Trivy em si só
acontece quando aurumcode sbom decide gerar. A etapa extrai o binário
aurumcode já compilado na imagem aurumcode-review (docker create/
docker cp — nenhum segundo build), gera (via heredoc com delimitador
citado, sem interpolação, mais um sed para injetar o valor pinado) um
wrapper trivy-docker.sh que reproduz o argv que TrivyGenerator já
constrói (fs --format <f> --output <p> <raiz> / image --format <f>
--output <p> <imagem>) dentro de docker run contra vuln_scanner_image
(hoje
docker.io/aquasec/trivy@sha256:7cced7cae583819fc7806d4cbc0dbbc7cad18b99f7d3e235192e6da8c091045c,
de .board/bootstrap/locks/scanners.yml), e então chama aurumcode sbom
--repo . --trivy-bin trivy-docker.sh DIRETO NO RUNNER (nunca de dentro de
outro container): rodar docker run de dentro de um container através de
um socket do Docker montado criaria um container IRMÃO que precisa de
caminhos do HOST para seus próprios -v, e nada dentro daquele container
sabe genericamente o caminho do host atrás do seu próprio
/github/workspace — por isso esta etapa evita se aninhar desde o
início, chamando docker run apenas do processo do job no runner.
Revisão desta rodada, segunda correção: o wrapper NÃO monta mais
.aurumcode-target (a árvore checked-out) como gravável dentro do
container do Trivy. A saída do Trivy dentro do container sempre aponta
para um diretório descartável criado em $RUNNER_TEMP (mktemp -d),
montado leitura-e-escrita; o próprio wrapper, já de volta no runner
(nunca dentro do container), move (mv) o arquivo pronto para o caminho
que aurumcode sbom pediu, só depois que o docker run termina, e limpa
o diretório descartável (trap ... EXIT). A montagem da árvore escaneada
(trivy fs, modo fs) continua só leitura, inalterada.
A sintaxe YAML+heredoc foi verificada localmente: python3 -c "import
yaml..." confirma o parse do review.yml e extrai a string do run:
inalterada; bash -n confirma a sintaxe do script extraído e do wrapper
gerado; uma simulação com um docker falso no PATH confirma que o
wrapper reconstrói exatamente os dois docker run esperados (fs e
image) com os mounts corretos. NÃO foi validado contra uma execução real
do GitHub Actions (sem acesso a CI neste ambiente) — um reviewer deveria
considerar isso antes do primeiro merge que dependa desta etapa.
action.yml (a action standalone, using: docker) NÃO roda aurumcode
sbom: seu próprio container não tem como traduzir seu /github/workspace
para o caminho equivalente no HOST, que um docker run aninhado através
do socket do Docker precisaria. Documentado como comentário no próprio
action.yml.
Lacunas conhecidas (reportadas, não resolvidas por este card)
- A etapa de CI em
review.ymlnão foi exercitada contra uma execução real do GitHub Actions (ver seção acima) — validada por inspeção, YAML/bash sintático e uma simulação local comdocker/aurumcodefalsos. action.ymlcontinua sem gerar SBOM (limitação estrutural documentada, não um lapso) — um card futuro teria que resolver a tradução de caminho host/container para viabilizar isso de dentro de uma action standalone.Dockerfilenão foi alterado: a geração de SBOM em CI não depende de nada instalado na imagem do produto, só dodockerdo próprio runner.