AUR-521: trilha de auditoria e SARIF para o code scanning da org
Contrato
Qualquer aurumcode review (--base ou --pr) pode escrever dois arquivos
adicionais, cada um só quando a flag correspondente é dada (zero flags =
comportamento de hoje, byte a byte):
--auditoria <arquivo>: um registro JSON (internal/render.AuditRecord) com o digest da política, o SHA do workflow, o repositório, o SHA revisado, o modelo, o veredito, a decisão do gate (pass/fail/inconclusive+ motivo), os achados que efetivamente reprovaram o gate (blocking_findings), as exceções aplicadas (exceptions_applied, sempre uma lista — vazia até a AUR-520 passar a preenchê-la) e a cobertura (completa ou não, com os arquivos que ficaram de fora).--sarif <arquivo>: um documento SARIF 2.1.0 (internal/render. BuildSARIFLog/WriteSARIF) comtool.driver.rules(um porrule_idcitado, com o título da seção de skill quando dinâmico),resultscomruleId,level,message,location(URI relativo ao repositório +startLine) epartialFingerprintscom a impressão digital canônica do achado. Uma revisão inconclusiva ainda produz um SARIF estruturalmente válido:invocations[0].executionSuccessful=falsee umtoolExecutionNotificationsnomeando o motivo (AC-004).
Ver docs/configuration.md's "Trilha de auditoria e SARIF" section para a
sintaxe completa.
A identidade canônica de achado (compartilhada com a AUR-494)
internal/render.FindingFingerprint (internal/render/finding_identity.go)
é a ÚNICA definição de "mesmo achado" neste projeto. Ela existe antes da
AUR-494 chegar de propósito: o card pede explicitamente que a impressão
digital seja "a mesma identidade de findings da AUR-494", então esta entrega
a define uma vez, documentada como canônica, para que a AUR-494 a importe em
vez de redefinir a sua própria.
A identidade é RuleID + caminho normalizado + linha + contexto de código
normalizado (sha256, hex). Nunca inclui:
- o texto livre do modelo isoladamente (
message) — o modelo reformula a mesma explicação de execução para execução, o que faria a impressão digital variar sem o achado ter mudado; - qualquer valor por chamada (relógio, nonce, PID) — exatamente o que MUT-001 ataca: uma impressão digital que muda a cada rodada para o MESMO achado quebra toda deduplicação/rastreamento por impressão digital (SARIF incluso).
normalizeFindingPath/normalizeFindingContext tornam a identidade estável
sob reformatação inofensiva (barra/backslash, ./, espaços) sem esconder
uma mudança real de conteúdo.
Crucialmente, o Context usado pela identidade NUNCA vem de
issue.Evidence/issue.Message (campos escritos pelo modelo e reescritos
pelo motor de revisão, redactLinesKeepingMarkers, antes de chegar aqui) —
render.FindingIdentityFor(diff, issue, filter)
(internal/render/finding_identity_diff.go) lê a linha real do hunk do
PRÓPRIO diff revisado em (issue.File, issue.Line, issue.Side). Uma revisão
independente deste card confirmou por teste decisivo
(TestAUR521AuditFingerprintStableAcrossTwoRuns, duas rodadas do MESMO diff
com message/evidence diferentes) que a versão inicial — que usava
issue.Evidence — produzia impressões digitais diferentes para o mesmo
achado sempre que o texto do modelo mudava, o que é exatamente o defeito que
este card existe para evitar. O Context passa por filter.Redact antes de
alimentar o hash: um segredo literal na linha revisada não pode fazer o
MESMO achado gerar impressões digitais diferentes só porque o segredo foi
rotacionado entre duas execuções.
Decisões de design
- A decisão do gate tem UMA fonte, e
evaluateGateé ela. Uma revisão independente apontou que a primeira versão desta entrega relia nummatchedBlockingFindingsque RE-DERIVAVA o loop de limiar emcmd/aurumcode/aur521.go— uma segunda implementação da mesma decisão, com risco real de divergir da primeira (e, de fato, da AUR-520, que chegou com seu próprio loop de exceção dentro do MESMO loop). Corrigido:evaluateGate(cmd/aurumcode/policygate.go) agora retornagateDecision.BlockingFindingsegateDecision.AppliedExceptions([]render.AuditFinding/[]render.AuditException) preenchidos NO EXATO ponto em queBreach/a exceção ativa já são decididos — nunca em um segundo loop.matchedBlockingFindingsfoi removido por completo;writeComplianceArtifactssó lêin.gate.BlockingFindings/AppliedExceptions. A mesma lista de exceções aplicadas também indexa quais achados o SARIF marca comosuppressions(achado excepado nunca aparece emblocking_findingsE aparece suprimido no SARIF, com a mesma justificativa — testado de ponta a ponta,TestAUR521ExceptedFindingSuppressedNotBlocking).auditGateOutcomenomeia a decisão (pass/fail/inconclusive) a partir degateDecisionMAIS o motivo de inconclusivo já calculado pelo chamador (gateInconclusiveReason), porque uma execução pode ser inconclusiva com NENHUM gate declarado (gate:ausente do config) — e, nesse caso, o SARIF da mesma execução já marcaexecutionSuccessful=false; os dois arquivos têm que concordar (testado,TestAUR521AuditAndSARIFAgreeOnInconclusive). AppliedExceptionssó é gravado achado por achado DENTRO do loop de limiar — e esse loop só roda quandogate.fail_onestá declarado.evaluateGatecheca a exceção de cada achado dentro do MESMOforque o limiar de severidade usa;gate.Threshold()retornaok=false(e a função volta antes de entrar nofor) quando a política não declaroufail_onnenhum, mesmo quegate.fail_onesteja ausente masexceptions/inconclusiveestejam presentes. Nesse casoAppliedExceptionsfica vazio mesmo que uma exceção real cobrisse um achado do diff — a exceção simplesmente nunca é avaliada, porque o gate em si nunca chega a avaliar achado nenhum. Isso é consistente com o próprio desenho da AUR-520 (uma exceção só importa onde há um limiar para ela evitar), mas significa queexceptions_appliednão é "toda exceção configurada que bateria com o diff" — é "toda exceção que o gate, rodando de verdade, realmente aplicou".- SARIF: redigir o
PathANTES denormalizeFindingPath, não depois. Defeito real encontrado numa terceira revisão:normalizeFindingPathtroca toda barra invertida literal por barra normal antes deWriteSARIFredigir o documento inteiro (redactSARIFLog) — então um segredo com uma barra invertida dentro doPathjá perdeu a forma exata que o filtro tinha registrado (a barra invertida) no momento em que a redação roda, e sobrevive, com barras normais, dentro deartifactLocation.uri. Corrigido com uma passada extra,redactSARIFFindings, que redige cada campo do achado (Path incluso) ANTES deBuildSARIFLogsequer normalizar o caminho —WriteSARIFagora tem três passadas de redação (achado crú, documento construído, texto final serializado), cada uma pegando uma forma que as outras não pegam. Testado (TestAUR521SARIFRedactsSecretWithBackslashInPath). - Redação ANTES de serializar, mais a passada final no sink. Outro
defeito real da primeira versão: redigir só o JSON já serializado
(
filter.Redact(string(data))) não pega um segredo cujo",\ou quebra de linha embutida ojson.Marshalescapou para uma sequência de bytes diferente da que os regexes de redação casam contra o valor CRU do segredo.redactAuditRecord/redactSARIFLog(internal/render/audit.go/sarif.go) agora redigem cada campo string individualmente ANTES dejson.MarshalIndent— construindo slices/ structs novos, nunca mutando o valor do chamador — e a passada final sobre o texto completo continua depois, como defesa em profundidade (nunca removida). Testado com três formas de segredo escapável (",\, quebra de linha) em ambos os arquivos (TestAUR521WriteAuditRecordRedactsEscapedSecrets,TestAUR521SARIFRedactsEscapedSecrets). exceptions_appliedsempre presente, agora POPULADO pela AUR-520 real.BuildAuditRecordnunca o deixanil/omitido; com a AUR-520 já nomainmesclado nesta entrega, o campo deixou de ser "vazio até a AUR-520" e passou a carregar a exceção real queevaluateGateaplicou (dono, motivo, validade), e a mesma exceção aparece suprimida no SARIF.omitted_fileshoje é a união deIgnoredPathseFilteredPaths(reviewCoverageBreakdown,cmd/aurumcode/passes.go) — os arquivos config-ocultados e os binários/grandes demais, ambos já rastreados por caminho. Omissões por orçamento de tokens (Budget) só chegam como contagem (code_files_omitted), nunca por caminho, porque o construtor de prompt nunca nomeia esses arquivos de volta paracmd/aurumcode` — uma lacuna pré-existente deste card, não uma introduzida por ele.modelé--modelo/opts.modelo, comLLM_MODELcomo alternativa (firstNonEmpty) —.github/workflows/review.ymlnunca passa--modelo, só a envLLM_MODEL; sem essa alternativa,modelsairia sempre vazio em todo run de CI real. Ainda não é o nome resolvido pelo PROVEDOR (que pode escolher o padrão do endpoint quando os dois ficam vazios) — documentado como melhor esforço: o card não exige uma segunda chamada ao provedor só para nomear o modelo de volta.workflow_shalê sóAURUMCODE_WORKFLOW_SHA, sem alternativa paraGITHUB_SHA.GITHUB_SHAé reservado pelo GitHub Actions num eventopull_request(o merge commit sintético; ver o comentário depr.go~linha 58/996, AUR-504) e umenv:de step não consegue sobrescrevê-lo — o mesmo é esperado paraGITHUB_WORKFLOW_SHA, embora isso não tenha sido confirmado por execução real (offline). Por isso o workflow reutilizável reenvia o SHA do próprio workflow sob um nome que a plataforma não reserva (AURUMCODE_WORKFLOW_SHA: ${{ job.workflow_sha }}). A versão anterior desta entrega caía paraGITHUB_SHAquandoAURUMCODE_WORKFLOW_SHAestava vazio — masGITHUB_SHAjá tem seu próprio campo (reviewed_sha); essa alternativa faziaworkflow_shasilenciosamente DUPLICAR o SHA revisado sempre que a CLI corresse fora deste workflow (um--baselocal, por exemplo), escondendo que a versão do workflow é simplesmente desconhecida. Corrigido: semAURUMCODE_WORKFLOW_SHA,workflow_shasai vazio, nunca igual areviewed_shapor acaso.action.ymlnão foi tocado.action.ymlinvoca o binário por argumentos posicionais fixos quescripts/action-entrypoint.shconsome — e esse script está fora dospathsdo card (só emread_paths). Adicionar--auditoria/--sarifpor esse caminho exigiria editar o entrypoint; a trilha de auditoria/SARIF é, por desenho do card ("a política de segurança... vem embutida no workflow reutilizável exigido pela org"), uma responsabilidade do workflow reutilizável, não da Action Docker direta..github/workflows/review.ymljá chama o binário com uma lista de argumentos explícita (não posicional) e foi estendido ali.diffLineAt(internal/render/finding_identity_diff.go) trata a marca\ No newline at end of filecomo qualquer outra linha. Um defeito real: odefault:do switch tratava QUALQUER linha que não começasse com+/-como uma linha de contexto (' '), avançando os dois contadores de linha — incluindo a própria marca "sem newline no final do arquivo" do git, que não é uma linha de diff nenhuma. O resultado era deslocar por um todo número de linha depois dela, mapeando um achado para o CONTEÚDO ERRADO (ou para nada). Corrigido para tratar' 'explicitamente (exatamente comointernal/review/scope.gojá faz no loop equivalente) e umdefault:que não avança nenhum contador — a mesma convenção, nunca uma segunda. Testado (TestAUR521DiffLineAtSkipsNoNewlineMarker).
Upload para o code scanning: NUNCA dentro do workflow reutilizável
A entrega anterior deste card fazia .github/workflows/review.yml chamar
github/codeql-action/upload-sarif ele mesmo, com security-events: write
no permissions: do próprio job. Uma revisão independente apontou o
defeito real disso: uma reusable workflow NÃO consegue conceder a si mesma
uma permissão que o CALLER não já tenha — GitHub recusa iniciar a chamada
inteira (não só o passo do upload) quando o workflow_call declara uma
permissão que o token do caller não tem. Como nenhum caller existente
(code-review.yml deste repositório, os exemplos, qualquer outro
repositório que já use este workflow) concedia security-events: write,
essa versão teria quebrado TODO caller no primeiro merge — não um upload
que falha com 403, o job de review inteiro nunca rodando.
Corrigido: review.yml nunca declara security-events: write e nunca
chama upload-sarif. Os dois arquivos saem como artefatos simples do job
(actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a,
v7.0.1 — a única action que este card ainda chama diretamente):
aurumcode-sarif-<PR> e aurumcode-audit-<PR>, ambos só quando o arquivo
correspondente não está vazio (o passo Check compliance artifacts are
non-empty, if: always(), testa -s antes — os dois arquivos são
pré-criados com : > só para o bind mount do Docker não criar um
diretório no lugar, e um upload nunca roda sobre um arquivo de 0 bytes) e
com if: always() nos dois uploads (um gate reprovado falha o passo "Run
and publish code review" — exatamente o caso em que o artefato mais
importa; sem always() ele seria pulado justo ali).
Quem quer o SARIF no code scanning de verdade roda um SEGUNDO job, no seu
próprio workflow — onde conceder security-events: write a si mesmo é
normal, sem cruzar fronteira de reusable workflow — que baixa o artefato e
chama github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2
(v4.38.2, SHA resolvido pelo coordenador via API do GitHub) com a
category fixa aurumcode-policy-gate (estável de propósito: um upload
--pr, ou um futuro run agendado, atualiza a MESMA análise no code
scanning em vez de acumular um conjunto de alertas que nunca é limpo). O
job exato está documentado em docs/configuration.md's "Trilha de
auditoria e SARIF" section; code-review.yml deste próprio repositório
ainda não tem esse segundo job (fora dos paths da AUR-521, não criado
por este card) — até que alguém o adicione, o SARIF deste repositório fica
disponível como artefato do job de review, mas não chega ao code scanning.
Outras lacunas conhecidas (declaradas, não corrigidas pelo prazo)
- AC-004 no
--pr, só quando o gate chega a rodar. Uma falha de provedor/transporte (llm.ErrAllProvidersFailed,llm.ErrBudgetExceeded) retorna derunPRReviewANTES do bloco do gate — e, portanto, antes dewriteComplianceArtifacts— exatamente como o comentário degateInconclusiveReasonempr.gojá documenta para o próprio gate ("NOTE: a genuine provider/transport failure... returns earlier in this function, before the gate is ever reached"). Nessa saída antecipada específica, com--auditoria/--sarifdados, o workflow fica com os dois arquivos vazios (pré-criados por: >emreview.yml) em vez de um registro/SARIF marcando inconclusivo. O caminho--base(runReview) não tem essa lacuna: toda saída antecipada equivalente já viragateInconclusiveReason = "provider_failure"antes do ponto onde este card escreve. Corrigir no--prexigiria uma segunda chamada awriteComplianceArtifactsnesses retornos antecipados especificamente; é acompanhado pela AUR-537. Não corrigido aqui.
Fechado nesta rodada: o canário de AC-005 agora também é testado na
LINHA DO DIFF (TestAUR521RedactsSecretCanaryFromDiffLine), não só no texto
do modelo — o caso que importa depois da mudança para
render.FindingIdentityFor, cujo Context vem do diff. Um achado cujo
message/evidence NUNCA mencionam o canário, mas cuja linha revisada o
contém, ainda não deixa o canário alcançar nenhum dos dois arquivos.
Testes
internal/render/finding_identity_test.go,internal/render/audit_test.go,internal/render/sarif_test.go: unidade, isolando cada escritor do gate/motor de revisão.cmd/aurumcode/aur521_test.go: comportamento real, através derunReview(--base) erunPRReview(--pr), no mesmo estilo deaur519_e2e_test.go.tests/acceptance/AUR-521.sh: seletoresall,AC-001..AC-005,AC-002-MUT-001(funde um nonce de relógio na impressão digital;TestAUR521FindingFingerprintStableAcrossRunsdeve virar RED), seletor desconhecido sai 64, falha de infraestrutura sai 79.