CodeQL Suppressions Ledger
Convenção operacional: Rust CodeQL ainda não suporta comentários inline
// codeql[rule]: justificationem 2026 (PR github/codeql#21638 aberto, sem merge). Este ledger é o mecanismo escolhido pelo projeto para registrar supressões — versionado em git, justificado por linha, auditável em PR review.Owner: GAR-491 (CodeQL Triage Wave 2). Plan:
personal-api-key-revogada-vectorized-matsumoto§Step 4. GAR-490 (Wave 1) extension: PR #111 (squash613510d) added 16 entries forrust/path-injectionafter empirical evidence that CodeQL's Rust pack does not modelvalidate_skill_name(crates/garraia-gateway/src/path_validation.rs) as a sanitizer. Each entry references the helper guard line, the dismissed-as-FP rationale, and the integration test that pins the rejection — see §4 alerts #67-#82. Last updated: 2026-08-30. Audit re-triage por: 2026-11-28 — 90 dias a partir da re-auditoria de 2026-08-30 (entradas com mais de 90 dias devem ser revisitadas; alertas que não existem mais no Security tab devem ser removidos do ledger).A data agora é verificada por código.
check-ledger-anchors.pycomparaaudit_expirationcom a data de hoje e sai com exit 7 se estiver no passado; o jobCodeQL Ledger Anchorsroda em todo PR. Até 2026-08-30 o campo não era lido por nada — nem script, nem workflow, nem teste — e ficou vencido 29 dias sem que nenhum build reclamasse. Uma regra sem leitor é um comentário.Removida em 2026-08-30 a entrada do alerta #43 (
credentials.rs:49,rust/hard-coded-cryptographic-value): o padrão que a justificava —let mut salt = vec![0u8; SALT_LEN];seguido derng.fill— deixou de existir. O site passou a usargarraia_security::random_bytes, que devolve o array já preenchido pelo CSPRNG e nunca materializa um buffer de zeros. É o mesmo desfecho do alerta #142 e a preferência declarada em §3: correção em código, não supressão. O ledger cai de 30 para 29 entradas. A §5 continua narrando o experimento empírico que usou o #43 como alvo — é registro histórico, e por isso a referência lá virou texto simples.Re-auditoria de mérito concluída em 2026-08-30 (issue #886), sobre as 29 entradas. A metade mecânica virou contínua com o
CodeQL Ledger Anchors(PR #882), então esta passagem cobriu o que ferramenta não faz — se cada justificativa ainda procede:
- Os 16
rust/path-injectionse apoiam emvalidate_skill_name(path_validation.rs:76) ser allow-list. Conferido: continuais_ascii_alphanumeric() || c == b'-'comMAX_NAME_LEN = 128, exatamente como o ledger alega. E é chamado antes de todos os 16 sinks, comreturnimediato no erro — verificado função a função, não por amostragem.- #111 / #112 (
config_cmd.rs) imprimem"set"/"not set"e uma lista de nomes de provider (.map(|(name, _)| name.clone())emcheck.rs:212), nunca a chave.- #113 (
wizard/mod.rs:769; era:673até o #1180 mover o wizard): okeyvai só paravault.set(entry, key); oprintln!interpola apenasentry. A correção de 2026-08-30 procede.- #115 (
session_store.rs:1326): o único valor ligado à query éparams![hash_session_token(token)], duas linhas abaixo.- #144 (
signal/mod.rs): reforçado desde a triagem — o POST de resposta do mesmotokio::spawnpassou a chamarapi::send_text, que roda o guard.- #145 / #146 (
whatsapp/api.rs):GRAPH_API_BASEé uma constantehttps://; não há caminho para tráfego em claro.- #40, #41, #42, #44, #45: continuam dentro de
#[cfg(test)].Nenhuma entrada mudou de categoria e nenhuma virou achado real. A #43 saiu por correção em código (acima), não por vencimento.
#165 — a condição de saída não é verificável neste repositório. A entrada diz que o
LEGACY_KDF_SALT"só sai quando não houver mais instalações por migrar". Confirmei que o código que o consome segue vivo:resolve_admin_encryption_keyé chamado no boot (server.rs:657e:820) e disparamigrate_admin_secrets_kdf. Mas se ainda existem instalações anteriores a 2026-08-29 sem ter bootado é fato de campo, não de código — o repositório não tem como responder. A entrada fica, e a condição de saída precisa de um critério observável (por exemplo: nenhuma instalação suportada em versão anterior à que introduziu o salt por instalação) antes de poder ser avaliada por quem for renovar em 2026-11-28.
§1. Background
GarraRUST adotou CodeQL advanced setup em 2026-04-30 (PR
#106, runbook em
docs/security/codeql-setup.md). O Security tab inicial
mostrou 90 alertas abertos. Esta sub-issue (GAR-491) endereça 6 alertas
do rule rust/hard-coded-cryptographic-value que estão estruturalmente em
test fixtures ou em buffer initializers — não são vulnerabilidades reais.
Tres alternativas avaliadas para suprimi-los:
| Mecanismo | Por que NÃO | Por que NÃO |
|---|---|---|
Inline // codeql[...] | ❌ não suportado em Rust | PR github/codeql#21638 aberto |
paths-ignore em codeql-config.yml | ❌ silencia arquivo inteiro | testes do GarraRUST são INLINE (#[cfg(test)] mod tests {}) dentro de produção; ignorar mobile_auth.rs esconde alertas reais |
query-filters: exclude por rule-id | ❌ silencia regra inteira | perde sinal de alertas reais futuros |
A solução adotada: REST API dismissal + este ledger versionado + script
de reaplicação (scripts/security/codeql-reapply-dismissals.sh).
Amendment 2026-08-29 — fixture de teste sai por escopo, não por ledger
A tabela acima descartou paths-ignore porque "testes do GarraRUST são INLINE
(#[cfg(test)] mod tests {}) dentro de produção; ignorar mobile_auth.rs
esconde alertas reais". Isso continua verdade para paths-ignore — mas existe
um mecanismo que resolve o problema pela raiz e que não estava em uso em
2026-05: o extractor Rust do CodeQL liga cfg(test) incondicionalmente
(rust/extractor/src/config.rs::to_cfg_overrides) e expõe a opção oficial
cargo_cfg_overrides para desligá-lo.
Desde 2026-08-29, .github/workflows/codeql.yml passa
CODEQL_EXTRACTOR_RUST_OPTION_CARGO_CFG_OVERRIDES: "-test" no passo
Initialize CodeQL. Consequência para este ledger:
- Fixture de teste não entra mais aqui. Se um alerta está dentro de um
#[cfg(test)]ou emcrates/*/tests/, ele deixa de existir por escopo de análise — não se dismissa, não se justifica linha a linha. - O ledger fica só para falso-positivo em código de produção, que é onde a
justificativa por linha realmente agrega (ex.:
credentials.rs:49, umvec![0u8; SALT_LEN]sobrescrito porSystemRandom::fillna linha seguinte). - As 5 entradas de
rust/hard-coded-cryptographic-valueque são fixture (mobile_auth.rs,validation.rs) continuam válidas — ao contrário do que esta seção previa antes de 2026-08-29. A previsão era que ficariam stale (exit 3) assim que o-testchegasse àmain; a medição mostrou o contrário, e a razão está em §5.2.credentials.rs:49é produção e permanece de qualquer forma. - O
-testcontinua valendo para alertas novos: fixture que ainda não virou alerta não vira mais. O que ele não faz é apagar alerta já dispensado. - As 16 entradas de
rust/path-injectionsão High e não são afetadas.
Contexto completo da onda que motivou a mudança (bundle 2.26.3 → 2.26.4,
cobertura de extração de 118 para 422 arquivos):
codeql-setup.md §"A onda de 2026-08-28".
A entrada nova desta leva já está registrada: alerta
#165, o salt PBKDF2 legado em
crates/garraia-gateway/src/admin/shared.rs:67 (LEGACY_KDF_SALT). O salt
constante deixou de ser usado para derivar qualquer chave nova — agora é
aleatório por instalação, com 600k iterações — mas a constante permanece no
fonte porque a migração forward-only precisa dela para decifrar os segredos já
gravados uma última vez antes de re-cifrá-los.
Ela estreia a disposição dismissed-wont-fix neste ledger, e a distinção
importa: as 22 entradas anteriores são false_positive (o CodeQL errou) ou
used_in_tests (não é superfície de produção). Esta é a primeira em que o
CodeQL está certo e mesmo assim mantemos o código — o valor é de fato um
salt constante, e a alternativa seria perder os segredos de toda instalação
existente. Registrar isso como falso-positivo seria mentir para o ledger.
Efeito medido do -test (PR #869, head 7a9b5fc): os alertas Critical
novos do PR caíram de 3 para 1, e o que apontava para um assert! dentro de
#[cfg(test)] passou a outdated. Sobrou exatamente o #165, que é produção.
§2. Mechanism
Cada alerta dismissed via:
gh api -X PATCH repos/michelbr84/GarraRUST/code-scanning/alerts/<N> \
-f state=dismissed \
-f dismissed_reason="<used_in_tests|false_positive|won't_fix>" \
-f dismissed_comment="GAR-491 — <justificativa>. See docs/security/codeql-suppressions.md row #<N>."
A fonte de verdade machine-readable é
docs/security/codeql-suppressions.json (schema
version 1.2.0). O script consome o JSON; este .md é a versão humana auditável.
Manter ambos sincronizados — o script tem flag --check-md que valida que
os números de alerta listados em §4 batem com entries[].alert_number do JSON.
Amendment 2026-09-19 — a âncora é o conteúdo, não a linha (issue #1263)
Até aqui a âncora de cada entrada era (path, line) e o
check-ledger-anchors.py conferia o sink_snippet naquela linha. Qualquer
commit que inserisse ou removesse linhas acima do sink derrubava o gate sem
encostar no statement suprimido. A entrada #113 andou duas vezes no mesmo
PR (#1252, :769 →
:826 → :867) sem o snippet mudar um byte — dois ciclos completos de uma
fila de ~110 jobs, para nada. Pior: o caminho mais rápido para ficar verde era
editar o sink_snippet até casar com o que estivesse na linha, ou seja,
fraudar o registro. Um guard cuja saída mais cômoda é a fraude depende de
disciplina, não de mecanismo.
Invertido: o sink_snippet é o identificador e o checker procura por ele no
arquivo, derivando a linha.
-
1 ocorrência → é ela. A linha derivada é escrita de volta no
.jsone na colunaFile:linedeste.md, e a saída dizlinha derivada X (antes Y). Moverlinenão é fraude: é posição derivada, não afirmação de auditoria — é justamente por isso que o script faz isso sozinho, e é por isso que editarsink_snippetà mão continua sendo. -
0 ocorrências → exit 6, pedindo reauditoria. É o único caso em que o gate deve ficar vermelho: o statement suprimido mudou ou sumiu.
-
2+ ocorrências → exit 5. O snippet não identifica um statement só, e escolher pela posição antiga seria voltar ao problema. A entrada precisa de âncora auxiliar, também de conteúdo:
"sink_snippet": "if !file_path.is_file() {","disambiguator": { "kind": "function", "value": "delete_skin" }kindhoje só tem um valor,function— o nome dafnRust envolvente, varrida para cima a partir do candidato. Deliberadamente não existe umkind"n-ésima ocorrência": ordinal reaponta em silêncio no dia em que alguém acrescenta um statement idêntico acima, que é a classe de bug que esta mudança fecha. Se duas ocorrências caírem na mesmafn, o gate acusa (exit 5) e pede âncora mais específica em vez de chutar.Na migração, 13 das 30 entradas eram ambíguas (
skills_handler.rstem quatroif !skill_path.exists() {idênticos;skins_handler.rsewhatsapp/api.rs, dois cada). Afnenvolvente da posição auditada foi única em todas as 13. A posição commitada foi confiada uma vez, e só aqui: o gate anterior exigia que o snippet casasse byte a byte com aquela linha, então olinedaquele commit era um ponteiro verificado. Daí em diante manda o conteúdo. -
Citações
arquivo:linhadentro dajustificationpassaram a ser conferidas. Foi só por falta disso que um drift sobreviveu escondido num documento de auditoria de segurança: a justificativa do #113 apontavacloud_secretem:630(estava em:824) e oeprintln!do vault em:643(estava em:837), e nenhum gate teria pego. Caminho ancorado no repo (crates/...) que não existe mais, ou linha fora de faixa em arquivo que existe, é exit 6; nome de arquivo solto que não resolve sai como aviso, porque prosa não é caminho e o script não adivinha. Citação:NNNsem arquivo fica fora de escopo de propósito — quase toda ocorrência é histórica ("o sink desceu de:1380para:1653") e validar história por faixa é tripwire sem valor.
O job do CI roda com --no-rewrite: reporta a posição derivada e não escreve
nada (a árvore do runner é descartada, e "reescrito" ali seria mentira). Quem
consertar roda o checker sem a flag, localmente, e commita o line
atualizado junto com a mudança que o moveu.
Wiring em workflow (2026-08-29) — fecha o GAR-491.2
O comando acima era executado à mão, de uma máquina com PAT de escopo
security_events — foi assim que os 22 alertas de GAR-490/GAR-491 foram
dispensados (§5). O header do script registrava a lacuna:
"Per amendment A8: there is intentionally NO schedule wiring here. (…) A future sub-issue (GAR-491.2) decides if/when to wire it into a workflow."
Agora existe .github/workflows/codeql-apply-dismissals.yml:
workflow_dispatch, permissions: security-events: write, rodando o mesmo
script com o GITHUB_TOKEN do job — sem PAT.
Três decisões que importam:
- Dry-run é o default. O input
applyéfalseaté alguém marcar. Rodar o workflow sem pensar não escreve nada. - Workflow separado do de triagem.
codeql-triage.ymlcontinuasecurity-events: read. Um relatório não carrega permissão de escrita sobre alertas de segurança. - Sem
schedule. Dispensar um alerta é ato deliberado, não rotina. E semcontinue-on-error:exit 2(ledger divergente) eexit 3(alerta sumiu) são o resultado útil quando acontecem — significam "reaudite à mão".
O input alert restringe a um número só, que foi como a prova empírica do #43
foi conduzida. Isso também contorna uma armadilha real: o loop percorre as
entradas em ordem e aborta na primeira stale, então uma entrada obsoleta no topo
impediria as de baixo de serem aplicadas.
§3. Operational rules
- No bulk suppression. Cada entrada precisa justificativa por linha.
- No silencing real alerts as FPs. Se em dúvida, NÃO suprime — abre sub-issue de investigação.
- Audit trail. Cada dismissal emite
dismissed_commentreferenciando GAR-# e linha do ledger. - Re-audit obrigatório a cada 90 dias. Audit expiration:
2026-08-01. Entradas vencidas devem ser revistas; se ainda válidas, renovar com nova justificativa + commit hash; se não, abrir fix real. - Fail-closed reaplicação. O script verifica
rule_id/path/linedo alerta atual contra o ledger antes de reaplicar. Se divergir → exit 2, manual re-audit obrigatório (alerta pode ter sido renumerado, código pode ter mudado, regra pode ter sido renomeada). - Sem fallback global. Se a empirical proof (§5) falhar, NÃO
recorrer a
query-filters: excludeglobal — abrir nova sub-issue para decidir entre custom query suite, path-specific approach, ou manual UI dismissal mantendo este ledger. alert_numbernão é chave estável — reconciliar antes de reaplicar. Ver "Amendment 2026-08-30" abaixo.
Amendment 2026-08-30 — renumeração silenciosa e o --rekey
Em 2026-08-29 o Security tab voltou a acusar 16 alertas rust/path-injection
que já estavam dispensados desde o PR
#111. Não foi regressão de
código, nem o dismissal ter sido revertido: o dry-run do
codeql-reapply-dismissals.sh no run
33261527354
reportou skipped: 23 (already-dismissed) — as entradas #67–#82 continuavam
vivas e dispensadas.
O que aconteceu foi duplicação. Quando o escopo da análise mudou no mesmo
dia (paths-ignore: crates/*/tests/** mais o
CODEQL_EXTRACTOR_RUST_OPTION_CARGO_CFG_OVERRIDES movido para o nível do job),
os partialFingerprints do SARIF mudaram junto, o GitHub deixou de casar os
achados com os alertas existentes e criou alertas novos — #147–#162 — para
o mesmo código, nas mesmas linhas. Os dismissals ficaram grudados nas
duplicatas aposentadas.
A lição: alert_number é um identificador de apresentação, não uma identidade.
A identidade estável é (rule_id, path, line) — que é, aliás, exatamente o que
o codeql-reapply-dismissals.sh já validava fail-closed antes de reaplicar.
Por isso existe scripts/security/codeql-rekey-ledger.py:
ele lista os alertas abertos, casa por (rule_id, path, line) e reaponta o
alert_number das entradas, reescrevendo .json e .md em sincronia para o
--check-md continuar passando. Ele não emite PATCH, não inventa
entrada para alerta sem justificativa, e não apaga entrada — nada nele
afrouxa a regra 1 (no bulk suppression): a justificativa de cada linha já foi
revisada uma vez e continua valendo, só o ponteiro muda, e o diff vai a review
em PR como qualquer outro.
Duas chaves iguais casando com dois alertas abertos é exit 2: o script não
adivinha. Entrada sem alerta aberto correspondente não é erro — é o estado
saudável de uma entrada dispensada e quieta —, só entra no resumo para alimentar
o re-audit de 90 dias da regra 4.
O codeql-triage.yml roda esse plano em --dry-run a cada execução e publica
no job summary, então a divergência aparece sozinha na próxima triagem em vez de
esperar alguém reparar no Security tab.
§4. Ledger
| # | Rule | File:line | Disposition | Reason | Justificativa | GAR |
|---|---|---|---|---|---|---|
| #40 | rust/hard-coded-cryptographic-value | crates/garraia-gateway/src/mobile_auth.rs:738 | dismissed-used-in-tests | used_in_tests | Test fixture em #[tokio::test] argon2id_register_and_login_roundtrip. Literal salt "" é placeholder do path PHC Argon2id (que embute seu próprio salt); coluna legacy não-usada. | GAR-491 |
| #41 | rust/hard-coded-cryptographic-value | crates/garraia-gateway/src/mobile_auth.rs:749 | dismissed-used-in-tests | used_in_tests | Test fixture em #[tokio::test] argon2id_register_and_login_roundtrip — branch negativo, password "nope" deve retornar false. Input intencionalmente inválido para coverage. | GAR-491 |
| #42 | rust/hard-coded-cryptographic-value | crates/garraia-gateway/src/mobile_auth.rs:870 | dismissed-used-in-tests | used_in_tests | Test fixture em #[tokio::test] second_login_after_upgrade_still_works. "seq-password-xyz" exercita o PBKDF2 → Argon2id lazy-upgrade transactional path; nunca persistido. | GAR-491 |
| #44 | rust/hard-coded-cryptographic-value | crates/garraia-security/src/validation.rs:233 | dismissed-used-in-tests | used_in_tests | Test fixture em #[test] validate_password_length. Literal "short" intencionalmente abaixo do mínimo para asserir Err. Negative-path coverage. | GAR-491 |
| #45 | rust/hard-coded-cryptographic-value | crates/garraia-security/src/validation.rs:234 | dismissed-used-in-tests | used_in_tests | Test fixture em #[test] validate_password_length. Literal "validpass123" intencionalmente acima do mínimo para asserir Ok. Positive-path coverage. | GAR-491 |
| #149 | rust/path-injection | crates/garraia-gateway/src/skins_handler.rs:84 | dismissed-false-positive | false_positive | GAR-490 PR A (PR #111, squash 613510d): create_skin guards body.name via validate_skill_name at line 60 before tokio::fs::create_dir_all / tokio::fs::write. Charset [A-Za-z0-9-]{1,128} ASCII-only. CodeQL Rust pack does not model the helper as a sanitizer. Regression: tests/skins_test.rs::create_skin_with_path_traversal_returns_400 + create_skin_with_dot_in_name_returns_400 + create_skin_rejects_underscore_per_project_convention. | GAR-490 |
| #147 | rust/path-injection | crates/garraia-gateway/src/skins_handler.rs:111 | dismissed-false-positive | false_positive | GAR-490 PR A (PR #111, squash 613510d): get_skin guards Path(name) via validate_skill_name at line 99 before format!("{name}.json"). Charset [A-Za-z0-9-]{1,128} ASCII-only. CodeQL Rust pack does not model the helper as a sanitizer. Regression: tests/skins_test.rs::get_skin_with_dot_in_name_returns_400. | GAR-490 |
| #148 | rust/path-injection | crates/garraia-gateway/src/skins_handler.rs:141 | dismissed-false-positive | false_positive | GAR-490 PR A (PR #111, squash 613510d): delete_skin guards Path(name) via validate_skill_name at line 129 before tokio::fs::remove_file. Charset [A-Za-z0-9-]{1,128} ASCII-only. CodeQL Rust pack does not model the helper as a sanitizer. Regression: tests/skins_test.rs::delete_skin_with_backslash_returns_400. | GAR-490 |
| #150 | rust/path-injection | crates/garraia-gateway/src/skills_handler.rs:177 | dismissed-false-positive | false_positive | GAR-490 PR A (PR #111, squash 613510d): get_skill guards Path(name) via validate_skill_name at line 162 before std::fs::read_to_string. Charset [A-Za-z0-9-]{1,128} ASCII-only. CodeQL Rust pack does not model the helper as a sanitizer. Regression: tests/skills_test.rs::get_skill_rejects_path_traversal. | GAR-490 |
| #153 | rust/path-injection | crates/garraia-gateway/src/skills_handler.rs:269 | dismissed-false-positive | false_positive | GAR-490 PR A (PR #111, squash 613510d): create_skill guards body.name via validate_skill_name at line 220 before std::fs::write. Charset [A-Za-z0-9-]{1,128} ASCII-only. CodeQL Rust pack does not model the helper as a sanitizer. Regression: tests/skills_test.rs::create_skill_rejects_{path_traversal,empty_name,nul_byte,windows_drive}. | GAR-490 |
| #154 | rust/path-injection | crates/garraia-gateway/src/skills_handler.rs:344 | dismissed-false-positive | false_positive | GAR-490 PR A (PR #111, squash 613510d): update_skill double-guards URL name + body.name via validate_skill_name at lines 300 and 307 before std::fs::write. Charset [A-Za-z0-9-]{1,128} ASCII-only. CodeQL Rust pack does not model the helper as a sanitizer. Regression: tests/skills_test.rs::update_skill_rejects_dot_in_name. | GAR-490 |
| #151 | rust/path-injection | crates/garraia-gateway/src/skills_handler.rs:533 | dismissed-false-positive | false_positive | GAR-490 PR A (PR #111, squash 613510d): export_skill guards Path(name) via validate_skill_name at line 519 before std::fs::read_to_string. Charset [A-Za-z0-9-]{1,128} ASCII-only. CodeQL Rust pack does not model the helper as a sanitizer. Regression: tests/skills_test.rs::export_skill_rejects_dot_in_name. | GAR-490 |
| #152 | rust/path-injection | crates/garraia-gateway/src/skills_handler.rs:590 | dismissed-false-positive | false_positive | GAR-490 PR A (PR #111, squash 613510d): set_skill_triggers guards Path(name) via validate_skill_name at line 574 before std::fs::read_to_string. Charset [A-Za-z0-9-]{1,128} ASCII-only. CodeQL Rust pack does not model the helper as a sanitizer. Regression: tests/skills_test.rs::set_skill_triggers_rejects_dot_in_name. | GAR-490 |
| #155 | rust/path-injection | crates/garraia-gateway/src/skills_handler.rs:632 | dismissed-false-positive | false_positive | GAR-490 PR A (PR #111, squash 613510d): set_skill_triggers guards Path(name) via validate_skill_name at line 574 before std::fs::write of updated trigger content. Charset [A-Za-z0-9-]{1,128} ASCII-only. CodeQL Rust pack does not model the helper as a sanitizer. Regression: tests/skills_test.rs::set_skill_triggers_rejects_dot_in_name. | GAR-490 |
| #161 | rust/path-injection | crates/garraia-gateway/src/skins_handler.rs:104 | dismissed-false-positive | false_positive | GAR-490 PR A (PR #111, squash 613510d): get_skin guards Path(name) via validate_skill_name at line 99 before tokio::fs::read_to_string. Charset [A-Za-z0-9-]{1,128} ASCII-only. CodeQL Rust pack does not model the helper as a sanitizer. Regression: tests/skins_test.rs::get_skin_with_dot_in_name_returns_400. | GAR-490 |
| #162 | rust/path-injection | crates/garraia-gateway/src/skins_handler.rs:134 | dismissed-false-positive | false_positive | GAR-490 PR A (PR #111, squash 613510d): delete_skin guards Path(name) via validate_skill_name at line 129 before file_path.is_file() check. Charset [A-Za-z0-9-]{1,128} ASCII-only. CodeQL Rust pack does not model the helper as a sanitizer. Regression: tests/skins_test.rs::delete_skin_with_backslash_returns_400. | GAR-490 |
| #156 | rust/path-injection | crates/garraia-gateway/src/skills_handler.rs:167 | dismissed-false-positive | false_positive | GAR-490 PR A (PR #111, squash 613510d): get_skill guards Path(name) via validate_skill_name at line 162 before skill_path.exists() check. Charset [A-Za-z0-9-]{1,128} ASCII-only. CodeQL Rust pack does not model the helper as a sanitizer. Regression: tests/skills_test.rs::get_skill_rejects_path_traversal. | GAR-490 |
| #157 | rust/path-injection | crates/garraia-gateway/src/skills_handler.rs:227 | dismissed-false-positive | false_positive | GAR-490 PR A (PR #111, squash 613510d): create_skill guards body.name via validate_skill_name at line 220 before skill_path.exists() check. Charset [A-Za-z0-9-]{1,128} ASCII-only. CodeQL Rust pack does not model the helper as a sanitizer. Regression: tests/skills_test.rs::create_skill_rejects_{path_traversal,empty_name,nul_byte,windows_drive,underscore_per_project_convention}. | GAR-490 |
| #158 | rust/path-injection | crates/garraia-gateway/src/skills_handler.rs:312 | dismissed-false-positive | false_positive | GAR-490 PR A (PR #111, squash 613510d): update_skill double-guards URL name + body.name via validate_skill_name at lines 300 and 307 before skill_path.exists() check. Charset [A-Za-z0-9-]{1,128} ASCII-only. CodeQL Rust pack does not model the helper as a sanitizer. Regression: tests/skills_test.rs::update_skill_rejects_dot_in_name. | GAR-490 |
| #159 | rust/path-injection | crates/garraia-gateway/src/skills_handler.rs:523 | dismissed-false-positive | false_positive | GAR-490 PR A (PR #111, squash 613510d): export_skill guards Path(name) via validate_skill_name at line 519 before skill_path.exists() check. Charset [A-Za-z0-9-]{1,128} ASCII-only. CodeQL Rust pack does not model the helper as a sanitizer. Regression: tests/skills_test.rs::export_skill_rejects_dot_in_name. | GAR-490 |
| #160 | rust/path-injection | crates/garraia-gateway/src/skills_handler.rs:579 | dismissed-false-positive | false_positive | GAR-490 PR A (PR #111, squash 613510d): set_skill_triggers guards Path(name) via validate_skill_name at line 574 before skill_path.exists() check. Charset [A-Za-z0-9-]{1,128} ASCII-only. CodeQL Rust pack does not model the helper as a sanitizer. Regression: tests/skills_test.rs::set_skill_triggers_rejects_dot_in_name. | GAR-490 |
| #111 | rust/cleartext-logging | crates/garraia-cli/src/config_cmd.rs:222 | dismissed-false-positive | false_positive | Wave 3 — cleartext. garraia config check imprime presenca, nunca valor: o campo summary.gateway_api_key_set e bool e as unicas strings possiveis sao os literais "set" / "not set". A secao sai sob o cabecalho literal "Summary (redacted)" (config_cmd.rs:206). CodeQL rastreia o identificador chamado api_key, nao o booleano que ele carrega. | GAR-491 |
| #112 | rust/cleartext-logging | crates/garraia-cli/src/config_cmd.rs:245 | dismissed-false-positive | false_positive | Wave 3 — cleartext. Mesma superficie do #111: imprime summary.llm_providers_api_key_set.join(", "), que e a lista de nomes de provider com chave configurada (ex.: [openai, anthropic]), nao as chaves. Invariante de redaction documentado em CLAUDE.md (garraia-config, plan 0046). | GAR-491 |
| #113 | rust/cleartext-logging | crates/garraia-cli/src/wizard/mod.rs:1005 | dismissed-false-positive | false_positive | Wave 3 — cleartext. println!(" Cloud provider API key encrypted in vault (entry {entry}).") imprime o nome da entrada do cofre, nunca o segredo. cloud_secret e Option<(&str, &str)> (wizard/mod.rs:828); o destructuring (entry, key) manda o key exclusivamente para vault.set(entry, key) na linha anterior, e o println! interpola apenas entry. CodeQL contamina entry por ele vir do mesmo tuple que key. Re-audit 2026-08-30: esta entrada apontava para :640 e descrevia outro statement — o eprintln! do vault open failed, hoje em :841. O alerta vivo sempre esteve no println! de :673, que e o que §5.3 ja dizia; era o .json que estava errado. Ver a nota de re-audit apos esta tabela. Reancorada 2026-09-16 (PR #1252): o sink desceu de :769 para :826 porque a geracao de credencial em bind exposto acrescentou linhas acima deste ponto. Mesmo statement, mesmo sink, sink_snippet intocado. Reancorada de novo 2026-09-16 (mesmo PR): o sink desceu de :826 para :867 quando a credencial deixou de ser impressa e a escrita com permissao restrita entrou acima deste ponto. As duas citacoes de linha da propria justificativa tambem estavam velhas e foram corrigidas junto: a declaracao de cloud_secret esta em :828 (era :630) e o eprintln! do vault em :841 (era :643). Mesmo statement, mesmo sink, sink_snippet intocado. Reancorada uma terceira vez 2026-09-16 (merge do #1256 com a main): o #1256 e o #1252 reancoraram esta MESMA entrada em paralelo — aquele por 4 linhas de roteamento do garra whatsapp, este pela geracao de credencial. Depois do merge as duas somam, entao a linha nao e a de nenhum dos dois lados: foi RECALCULADA na arvore mesclada para :871. As duas citacoes internas subiram os mesmos 4: cloud_secret em :828 e o eprintln! do vault em :841. Mesmo statement, mesmo sink, sink_snippet intocado. | GAR-491 |
| #115 | rust/cleartext-storage-database | crates/garraia-db/src/session_store.rs:1711 | dismissed-false-positive | false_positive | Triagem 2026-08-30. O que chega ao banco e um hash, nunca o token: validate_session_token liga params![hash_session_token(token)], e hash_session_token (session_store.rs:1612) e SHA-256 hex de mao unica. O lado da escrita declara o invariante em session_store.rs:1634 — "Only the hash is persisted; the raw token goes to the caller and is never written to disk" — e ha migracao que reescreve tokens legados em claro, detectados por length(token) <> 64 OR token GLOB '*[^0-9a-f]*' (session_store.rs:347). CodeQL trata token: &str como credencial fluindo para uma query, sem modelar o hash como transformacao irreversivel. Reancorada 2026-09-06 (PR #1007): o sink desceu de :1380 para :1653 porque set_agent_mode ganhou distincao entre modo escolhido e deduzido (#988), o upsert passou a mesclar metadado (#1008) e o modo customizado ganhou escopo de usuario (#986) — 273 linhas acima no total. Mesmo statement, mesma funcao, mesmo sink. Reancorada 2026-09-09 (PR #1114): o sink desceu de :1653 para :1674 porque o patch de metadado nao-objeto na criacao de sessao (#1102) inseriu 21 linhas em SessionStore, todas acima deste ponto. Reancorada 2026-09-15 (PR #1224): o sink desceu de :1674 para :1677 porque a migration da tabela agent_runs (ledger duravel de runs, P1 do gap analysis) inseriu 3 linhas no inicio de run_migrations. Mesmo statement, mesma funcao, mesmo sink_snippet. Reancorada 2026-09-20 (PR #1309): o sink subiu de :1677 para :1669 e o sink_snippet mudou de .prepare(&sql) para .prepare( porque a nota lateral da #1247 reescreveu a query: o SQL montado por format! virou literal estatico, com o timeout idle entrando por bind ?2. Mesmo statement, mesma funcao validate_session_token, mesmo invariante — o que chega ao banco segue sendo so o hash, agora em session_store.rs:1681. Nao ha mais SQL dinamico neste caminho. | GAR-491 |
| #144 | rust/cleartext-transmission | crates/garraia-channels/src/signal/mod.rs:228 | dismissed-false-positive | false_positive | Triagem 2026-08-30, reancorada neste PR. O sink (:228 aqui; o alerta vivo aponta :279, emitido contra a main antes de este PR encolher os sends para wrappers) esta dentro de connect(), depois do guard: connect() chama ensure_url_vetted()? antes de qualquer IO, e o ? aborta se a URL for recusada. O config e clonado antes do tokio::spawn e SignalConfig nao tem mutabilidade interior, entao a URL do loop e o mesmo valor imutavel ja validado. CodeQL nao liga o clone a validacao anterior. Auditado por security-auditor. Este PR reforca a justificativa: o POST de resposta dentro do mesmo spawn passou a chamar api::send_text, que roda o guard — antes ele dependia so da ordem de execucao. Contraste com o #143, mesma regra e mesmo arquivo, que nao entrou neste ledger: era codigo morto (receive_messages, #[allow(dead_code)], zero call sites) e foi removido, fechando como Fixed. | GAR-491 |
| #145 | rust/cleartext-transmission | crates/garraia-channels/src/whatsapp/api.rs:48 | dismissed-false-positive | false_positive | Wave 3 — cleartext. O destino e HTTPS: const GRAPH_API_BASE: &str = "https://graph.facebook.com/v21.0" (whatsapp/api.rs:5), entao .bearer_auth(token) sai sempre sobre TLS. O esquema e constante de compilacao e nenhum caminho de config o altera. CodeQL nao dobra a const dentro do format! e trata o esquema como desconhecido. | GAR-491 |
| #146 | rust/cleartext-transmission | crates/garraia-channels/src/whatsapp/api.rs:79 | dismissed-false-positive | false_positive | Wave 3 — cleartext. Identico ao #145, em mark_as_read. Contraste deliberado com os alertas 143/144 do canal Signal: mesmo rule, mas nao eram falso-positivo — la a URL base vinha da config do operador e admitia http:// remoto, e foi corrigida em codigo (guard vet_signal_cli_url). | GAR-491 |
| #165 | rust/hard-coded-cryptographic-value | crates/garraia-gateway/src/admin/shared.rs:67 | dismissed-wont-fix | wont_fix | LEGACY_KDF_SALT. Não é falso-positivo nem fixture — o CodeQL está certo, é um salt PBKDF2 constante. Permanece deliberadamente porque a migração forward-only do PR #869 precisa dele para decifrar uma última vez os segredos de instalações anteriores a 2026-08-29, antes de re-cifrá-los sob a chave derivada do salt aleatório por instalação. Nenhuma chave nova é derivada dele: derive_with_passphrase só o usa no arm de migração pendente e no fallback de parâmetros ilegíveis. Os parâmetros novos vivem na tabela kdf_params do admin.db, gravados na mesma transação que re-cifra os segredos. Só sai quando não houver mais instalações por migrar. ✅ Número reconferido na main em 2026-08-29 (squash a47d3c3): o dry-run casou rule_id/path/line sem exit 2, então 165 sobreviveu ao squash. Dispensado no mesmo dia — ver §5.2. | PR-869 |
| #176 | rust/cleartext-logging | crates/garraia-cli/src/whatsapp.rs:1367 | dismissed-false-positive | false_positive | PR #1256 (#1238). Mensagem de sucesso do garra whatsapp link: o que chega ao stdout é um caminho de diretório, não credencial e não PII — <data_dir>/whatsapp/<account>/, com <data_dir> saindo de AppConfig::resolved_data_dir() (a máquina do próprio usuário). A entrada faz duas afirmações, e cada uma tem prova própria. (1) O caminho não sai do data dir, porque for_data_dir devolve Result e recusa (SessionError::InvalidAccount) qualquer conta que não seja um segmento [A-Za-z0-9_-]{1,64} — allowlist que fecha .., /, \, ., vazio, NUL e espaço de uma vez. E for_data_dir é o único construtor que existe fora da crate: a irmã SessionStore::new, que aceita qualquer PathBuf e não valida nada, é pub(crate) desde a revisão R4 — quem impede a CLI de contorná-la é o rustc, não a disciplina de quem escreve o call site. Regressão da declaração: the_store_exposes_a_single_validating_constructor (source_scan.rs), que fica vermelho no dia em que new voltar a ser pub. Regressão: an_account_that_escapes_the_data_dir_is_refused_before_any_write (session.rs), que no ramo em que a validação aceitasse grava a sessão cifrada e afirma a contenção no data dir. (2) O segmento impresso é a constante DEFAULT_ACCOUNT — um número de telefone casaria a regra de (1) e seria PII no stdout, então validar não basta aqui. Regressão: every_session_store_in_the_cli_uses_the_constant_account (whatsapp/tests.rs), que varre a árvore inteira de crates/garraia-cli/src lida do disco e exige que o segundo argumento de cada for_data_dir seja literalmente DEFAULT_ACCOUNT, lendo o argumento até o parêntese que fecha a chamada — account.unwrap_or(DEFAULT_ACCOUNT) é reprovado, e o parser tem teste próprio. ⚠️ Reauditada 2026-09-16 (revisão R4): a versão anterior desta linha afirmava que o invariante estava pinado por um scan de janela de 160 caracteres num arquivo só; ele aprovava conta dinâmica com a constante como fallback e não enxergava call site em outro arquivo, e o for_data_dir não validava nada — o path traversal estava fechado por acidente. Guarda textual → validação estrutural. Por que supressão e não correção: o mesmo segmento já aparece em outros dois println! do comando — store.blob_path() no status e store.archive_path() no aviso de arquivado — e nenhum dos dois foi sinalizado; apagá-lo só neste sink calaria o scanner sem mudar nada do que ele teme, e dizer onde a sessão foi gravada é o motivo de a mensagem existir. Deixa de valer se a CLI passar a escolher conta em runtime: é o teste (2) que fica vermelho nesse dia. Reancorada 2026-09-16 (PR #1256): o sink desceu de :631 para :807 porque as revisões R3/R4 acrescentaram acima deste ponto o tratamento de erro do for_data_dir, o ArchiveGuard e a extração de link_paired — mesmo statement, sink_snippet intocado. Reancorada de novo 2026-09-17 (PR #1256, rodada 4): desceu de :807 para :994 — a rodada acrescentou acima dele o prazo do handshake, a saída injetável do ArchiveGuard e o comando garra whatsapp restore. Mesmo statement, sink_snippet intocado. Reancorada de novo 2026-09-17 (PR #1256, rodada 5): desceu de :994 para :1017 — a rodada acrescentou acima dele a prova de que o blob restaurado abre antes de restore ligar o canal. Mesmo statement, sink_snippet intocado. Reancorada de novo 2026-09-17 (PR #1256, rodada 6): desceu de :1017 para :1018 — a rodada inverteu a ordem daquela prova, que agora roda ANTES de mover o arquivado. Mesmo statement, sink_snippet intocado. Reancorada de novo 2026-09-17 (PR #1256, rodada 9): desceu de :1018 para :1067 — entrou acima dele o needs_a_terminal, a mensagem que link e cloud passaram a imprimir sem terminal, no lugar de repetirem o texto do menu e sairem 0. Linha recalculada nesta arvore, nao copiada. Mesmo statement, sink_snippet intocado. Reancorada de novo 2026-09-17 (PR #1256, rodada 10): desceu de :1067 para :1070 — entrou acima dele o pulso PairUi::connecting, que da sinal de vida enquanto a ponte esta muda antes do primeiro QR. Linha recalculada, nao copiada. Mesmo statement, sink_snippet intocado. Reancorada de novo 2026-09-17 (PR #1265, fatia D): desceu de :1070 para :1107 — o merge da fatia D entrou acima dele a integração de saúde do garra whatsapp status: o use do módulo health, DiskFacts::read + classify, o bloco de MissingDependencies e a função print_next_step. Linha recalculada nesta árvore com grep, não copiada. Mesmo statement, sink_snippet intocado. ⚠️ Número a reconferir após o merge — 173 foi atribuído na análise da branch. Reapontada 2026-09-25 (#173 → #176): confirmado depois do merge da v0.4.5 — o GitHub abriu o alerta #176 para o mesmo sink (span 1356-1358, sink em :1358; o println! virou multi-linha com o rustfmt) e o #173 ficou dispensado numa duplicata aposentada. Reapontamento feito pelo codeql-rekey-ledger.py, que passou a casar a linha do sink dentro do span do alerta, como o codeql-reapply-dismissals.sh já fazia. Mesmo statement, sink_snippet intocado. | PR-1256 |
| #178 | rust/path-injection | crates/garraia-channels/src/whatsapp_linked/bridge.rs:629 | dismissed-false-positive | false_positive | PR #1373 (v0.4.5). Mesma classe e mesmo diretório do #174: std::fs::metadata(&stamp) em deps_pending lê o carimbo de dependências (DEPS_STAMP_FILE, nome constante) dentro do diretório do bridge Node que o prepare recebe — PreparoDaPonte::embutido(paths.bridge_dir) no gateway e ctx.bridge_dir() na CLI, ambos derivados de AppConfig::resolved_data_dir() do config.yml do operador. Nenhum byte de request, canal ou LLM alcança o caminho; a leitura é só stat e no máximo os 7 bytes do marcador pending. Deixa de valer, junto com o #174, se o diretório do bridge passar a vir de request, canal ou LLM. | PR-1373 |
| #177 | rust/path-injection | crates/garraia-channels/src/whatsapp_linked/bridge.rs:631 | dismissed-false-positive | false_positive | PR #1373 (v0.4.5). Mesma classe e mesmo diretório do #174: std::fs::read(&stamp) em deps_pending lê o carimbo de dependências (DEPS_STAMP_FILE, nome constante) dentro do diretório do bridge Node que o prepare recebe — PreparoDaPonte::embutido(paths.bridge_dir) no gateway e ctx.bridge_dir() na CLI, ambos derivados de AppConfig::resolved_data_dir() do config.yml do operador. Nenhum byte de request, canal ou LLM alcança o caminho; a leitura é só stat e no máximo os 7 bytes do marcador pending. Deixa de valer, junto com o #174, se o diretório do bridge passar a vir de request, canal ou LLM. | PR-1373 |
| #181 | rust/path-injection | crates/garraia-gateway/src/rest_v1/uploads.rs:1081 | dismissed-false-positive | false_positive | Falso positivo em finalize_upload. Falso positivo: staging_path junta staging_dir (canonicalizado no bootstrap) com format!("{upload_id}.staging") e upload_id: Uuid tipado (crates/garraia-gateway/src/rest_v1/uploads.rs:197) — os handlers recebem Path(upload_id): Path<Uuid> (crates/garraia-gateway/src/rest_v1/uploads.rs:656 em patch_upload, crates/garraia-gateway/src/rest_v1/uploads.rs:875 em delete_upload) e o extractor do axum recusa valor que nao parseie como UUID (400 antes do handler). Forma canonica de 36 chars [0-9a-f-], sem traversal. CodeQL nao modela Path<Uuid> como sanitizer. Regressao: uploads.rs::staging_path_is_traversal_safe (crates/garraia-gateway/src/rest_v1/uploads.rs:1538). | GAR-491 |
| #182 | rust/path-injection | crates/garraia-gateway/src/rest_v1/uploads.rs:1093 | dismissed-false-positive | false_positive | Falso positivo em finalize_upload. Falso positivo: staging_path junta staging_dir (canonicalizado no bootstrap) com format!("{upload_id}.staging") e upload_id: Uuid tipado (crates/garraia-gateway/src/rest_v1/uploads.rs:197) — os handlers recebem Path(upload_id): Path<Uuid> (crates/garraia-gateway/src/rest_v1/uploads.rs:656 em patch_upload, crates/garraia-gateway/src/rest_v1/uploads.rs:875 em delete_upload) e o extractor do axum recusa valor que nao parseie como UUID (400 antes do handler). Forma canonica de 36 chars [0-9a-f-], sem traversal. CodeQL nao modela Path<Uuid> como sanitizer. Regressao: uploads.rs::staging_path_is_traversal_safe (crates/garraia-gateway/src/rest_v1/uploads.rs:1538). | GAR-491 |
| #183 | rust/path-injection | crates/garraia-gateway/src/rest_v1/uploads.rs:1248 | dismissed-false-positive | false_positive | Falso positivo em append_to_staging (crates/garraia-gateway/src/rest_v1/uploads.rs:1244): o parametro vem do chamador via staging.staging_path(upload_id) (crates/garraia-gateway/src/rest_v1/uploads.rs:755, crates/garraia-gateway/src/rest_v1/uploads.rs:979, crates/garraia-gateway/src/rest_v1/uploads.rs:1075), nunca de dado cru. Falso positivo: staging_path junta staging_dir (canonicalizado no bootstrap) com format!("{upload_id}.staging") e upload_id: Uuid tipado (crates/garraia-gateway/src/rest_v1/uploads.rs:197) — os handlers recebem Path(upload_id): Path<Uuid> (crates/garraia-gateway/src/rest_v1/uploads.rs:656 em patch_upload, crates/garraia-gateway/src/rest_v1/uploads.rs:875 em delete_upload) e o extractor do axum recusa valor que nao parseie como UUID (400 antes do handler). Forma canonica de 36 chars [0-9a-f-], sem traversal. CodeQL nao modela Path<Uuid> como sanitizer. Regressao: uploads.rs::staging_path_is_traversal_safe (crates/garraia-gateway/src/rest_v1/uploads.rs:1538). | GAR-491 |
| #184 | rust/path-injection | crates/garraia-gateway/src/rest_v1/uploads.rs:811 | dismissed-false-positive | false_positive | Falso positivo em patch_upload. Falso positivo: staging_path junta staging_dir (canonicalizado no bootstrap) com format!("{upload_id}.staging") e upload_id: Uuid tipado (crates/garraia-gateway/src/rest_v1/uploads.rs:197) — os handlers recebem Path(upload_id): Path<Uuid> (crates/garraia-gateway/src/rest_v1/uploads.rs:656 em patch_upload, crates/garraia-gateway/src/rest_v1/uploads.rs:875 em delete_upload) e o extractor do axum recusa valor que nao parseie como UUID (400 antes do handler). Forma canonica de 36 chars [0-9a-f-], sem traversal. CodeQL nao modela Path<Uuid> como sanitizer. Regressao: uploads.rs::staging_path_is_traversal_safe (crates/garraia-gateway/src/rest_v1/uploads.rs:1538). | GAR-491 |
| #185 | rust/path-injection | crates/garraia-gateway/src/rest_v1/uploads.rs:980 | dismissed-false-positive | false_positive | Falso positivo em delete_upload. Falso positivo: staging_path junta staging_dir (canonicalizado no bootstrap) com format!("{upload_id}.staging") e upload_id: Uuid tipado (crates/garraia-gateway/src/rest_v1/uploads.rs:197) — os handlers recebem Path(upload_id): Path<Uuid> (crates/garraia-gateway/src/rest_v1/uploads.rs:656 em patch_upload, crates/garraia-gateway/src/rest_v1/uploads.rs:875 em delete_upload) e o extractor do axum recusa valor que nao parseie como UUID (400 antes do handler). Forma canonica de 36 chars [0-9a-f-], sem traversal. CodeQL nao modela Path<Uuid> como sanitizer. Regressao: uploads.rs::staging_path_is_traversal_safe (crates/garraia-gateway/src/rest_v1/uploads.rs:1538). | GAR-491 |
| #186 | rust/path-injection | crates/garraia-agents/src/tools/session_workspace.rs:313 | dismissed-false-positive | false_positive | Falso positivo em diretorio_de_verdade (disambiguator diretorio_de_verdade): chamador quente passa self.raiz, o workspace do operador verificado no boot; a linha e checagem read-only com symlink_metadata que nao segue o link — a defesa, nao a vulnerabilidade. Falso positivo: o caminho e raiz.join(nome_do_subdiretorio(session_id)) — 32 hex do digest SHA-256 do id (crates/garraia-agents/src/tools/session_workspace.rs:220), charset sem .., sem separador, sem absoluto; o outro chamador e o boot do gateway sobre o data_dir canonicalizado do operador. CodeQL nao modela SHA-256-hex como sanitizer. Regressao: session_workspace.rs::o_caminho_fica_sempre_debaixo_da_raiz e session_workspace.rs::o_nome_do_subdiretorio_e_sempre_hex_de_32_nunca_escapa_charset (crates/garraia-agents/src/tools/session_workspace.rs:453). | GAR-491 |
| #187 | rust/path-injection | crates/garraia-agents/src/tools/session_workspace.rs:335 | dismissed-false-positive | false_positive | Falso positivo em garantir_raiz (disambiguator garantir_raiz): mesma origem do #188. A linha e a checagem read-only com symlink_metadata (recusa Symlink sem seguir) — a defesa contra traversal; o mkdir de componente so, 0700, vem logo abaixo. Falso positivo: o caminho e raiz.join(nome_do_subdiretorio(session_id)) — 32 hex do digest SHA-256 do id (crates/garraia-agents/src/tools/session_workspace.rs:220), charset sem .., sem separador, sem absoluto; o outro chamador e o boot do gateway sobre o data_dir canonicalizado do operador. CodeQL nao modela SHA-256-hex como sanitizer. Regressao: session_workspace.rs::o_caminho_fica_sempre_debaixo_da_raiz e session_workspace.rs::o_nome_do_subdiretorio_e_sempre_hex_de_32_nunca_escapa_charset (crates/garraia-agents/src/tools/session_workspace.rs:453). | GAR-491 |
| #188 | rust/path-injection | crates/garraia-agents/src/tools/session_workspace.rs:352 | dismissed-false-positive | false_positive | Falso positivo em criar_fechado: mkdir de um componente so, sem recursive, nasce 0700; link plantado faz mkdir falhar com EEXIST em vez de ser seguido. Falso positivo: o caminho e raiz.join(nome_do_subdiretorio(session_id)) — 32 hex do digest SHA-256 do id (crates/garraia-agents/src/tools/session_workspace.rs:220), charset sem .., sem separador, sem absoluto; o outro chamador e o boot do gateway sobre o data_dir canonicalizado do operador. CodeQL nao modela SHA-256-hex como sanitizer. Regressao: session_workspace.rs::o_caminho_fica_sempre_debaixo_da_raiz e session_workspace.rs::o_nome_do_subdiretorio_e_sempre_hex_de_32_nunca_escapa_charset (crates/garraia-agents/src/tools/session_workspace.rs:453). | GAR-491 |
Total: 30 entries (5 from GAR-491 Wave 2 + 16 from GAR-490 Wave 1 PR A +
1 from PR #869 + 5 from
Wave 3 cleartext + 2 da triagem de 2026-08-30, #115 e #144 + 1 do PR
#1256).
+8 na auditoria CodeQL de 2026-10-10 (alertas #181–#188). Três em
crates/garraia-agents/src/tools/session_workspace.rs(#186–#188) e cinco emcrates/garraia-gateway/src/rest_v1/uploads.rs(#181–#185), todosrust/path-injectionseverity=high. Veredito por entrada: falso-positivo — o dado nao-confiavel (session_id, upload-id de URL) so alcanca o filesystem como SHA-256-hex de 32 chars ou UUID canonico, e nenhum dos dois charset expressa traversal; o CodeQL Rust pack nao modela nenhuma das duas transformacoes como sanitizer. Regressao nova:o_nome_do_subdiretorio_e_sempre_hex_de_32_nunca_escapa_charset. Removida em 2026-10-10 (mesma missão): a entrada #179 saiu do ledger porque o alerta vivo estástate=fixed— o--applytrata entrada de alerta resolvido comoexit 3(stale) e aborta fail-closed; remoção é o caminho (precedente: #143). Total hoje: 40 entries (33 + 8 − 1).
Correção de contagem, 2026-09-16 (PR #1256). A
maindeclarava 31 entries com 29 linhas na tabela, e a parcela "GAR-491 Wave 2" dizia 6 onde há 5 — a soma nunca fechou. Este PR acrescentou uma entrada (#173) e corrigiu a declaração para 30, que é o número que a tabela sustenta (5 + 16 + 1 + 5 + 2 + 1 = 30). O31 → 30no diff, portanto, não é uma entrada removida: é a soma passando a bater. Registrado porque num ledger de alertas de segurança dispensados uma contagem que muda calada é exatamente a espécie de número que ninguém reconfere. Sink novo a esperar depois do merge, 2026-09-17 (PR #1256, rodada 6).crates/garraia-channels/src/whatsapp_linked/session.rs:694ganhou umtracing::warn!(caminho = %tmp.display(), …)— a rodada 5 o acrescentou para que a falha ao triturar o temporário da sessão deixasse rastro em vez de sumir em silêncio. Ele tem a mesma forma do #173: o que vai para o log é um caminho, e o conteúdo (o blob cifrado) nunca. O ledger foi reancorado só para o #173, porque é o único alerta que a análise da branch emitiu até aqui; a análise pós-merge roda sobre outra base e pode abrir um alertarust/cleartext-loggingnovo neste segundo sink. Fica registrado aqui para que ele chegue como coisa prevista, com a justificativa já pronta, e não como surpresa a ser despachada às pressas. Quem o triar: o argumento é o mesmo do #173 — caminho sob o data dir da própria máquina, conta fixa emDEFAULT_ACCOUNT— e a diferença é que aqui nem o segmento de conta é escolhido em runtime, porquetmpé derivado deblob_path().
Bulk-dismissal proibido — cada linha foi revisada individualmente, com referência ao helper guard, ao handler afetado, e à regressão de teste correspondente.
Triagem 2026-08-30 — os 3 alertas que sobraram sem entrada
Depois da renumeração e do apply dos 21 dismissals, o --dry-run do
codeql-rekey-ledger.py deixou exatamente 3 alertas abertos sem entrada:
#115, #143 e #144. Os três foram auditados, e o desfecho não foi
o mesmo para os três — que é o ponto de auditar um a um.
#143 saiu por remoção de código, não por supressão. O sink era
receive_messages() em signal/mod.rs, marcada #[allow(dead_code)], com zero
call sites em todo o workspace, duplicando a construção de URL e o GET que o loop
dentro de connect() já faz. Ela também era uma armadilha: não passava pelo
guard, ao contrário do loop. Foi removida, e o alerta fecha como Fixed.
Nenhuma entrada foi criada — o ledger é último recurso (§3), e código morto não
precisa de justificativa, precisa de git rm.
#144 e #115 são falso-positivo de verdade e entraram na tabela acima, cada um com a evidência de linha.
O que a auditoria destapou de brinde. O doc comment do próprio
vet_signal_cli_url afirmava cobrir "every request this channel makes",
nomeando send_text / send_to_group porque carregam o corpo da mensagem. Mas o
guard tinha um único call site: connect(). E Channel::send_message alcança
os dois sends sem connect() jamais rodar — nada no tipo nem em runtime ordena os
dois. Era vazamento real de número de telefone e corpo de mensagem sobre http://
remoto, e o CodeQL não flagou (só apontou :143 e :272). Corrigido no mesmo
PR: os sends passam por ensure_url_vetted, que memoiza o veredito num
OnceLock — o guard resolve DNS com syscall bloqueante, e SignalConfig é
imutável depois de construída, então pagar isso a cada envio seria caro e inútil.
Mitigante que a auditoria também estabeleceu: SignalChannel hoje não é
instanciado em lugar nenhum — bootstrap/channels.rs não tem arm "signal".
Risco imediato baixo, risco latente alto, que é exatamente o caso em que se
fecha a porta antes de alguém abrir.
Re-audit 2026-08-30 — a renumeração aplicada, e o que ela destapou
As 16 entradas rust/path-injection foram reapontadas dos números antigos
(#67-#82) para as duplicatas vivas (#147-#162), fechando o que a
Amendment 2026-08-30
previa. A condição que §5.3 impunha para adiar — "as linhas vão mudar de novo,
por causa do autofix do #874" — deixou de valer: o dry-run do run
33299044785,
sobre c7d7b08, casou as 16 por (rule_id, path, line) sem ambiguidade.
Nenhuma justificativa foi tocada: só o ponteiro andou, que é o que mantém a
regra §3.1 de pé.
O #113 não era renumeração — era entrada errada. O .json apontava para
wizard/mod.rs:640 e descrevia o eprintln! do vault open failed. Esse
eprintln! está hoje em :643 (deriva de +3), mas o alerta vivo está em
:673, que é outro statement, em outro bloco: o
println!(" Cloud provider API key encrypted in vault (entry {entry}).").
Fosse o eprintln!, o alerta teria acompanhado a deriva de +3. A §5.3 acima já
descrevia o #113 corretamente ("imprime o nome da entrada do cofre") — ou seja,
.json e .md discordavam sobre o que a entrada era. Ambos foram corrigidos
para o sink real, e a conclusão de falso-positivo continua de pé, agora pela
razão certa.
Por que passou despercebido por três meses. plan_rekey pulava toda entrada
cujo alert_number seguisse aberto, sem nunca comparar a chave — o comentário
dizia "ja aponta para um alerta aberto nesta chave", mas o código só
comparava o número. Uma entrada com número vivo e linha podre era invisível ao
relatório. O script agora reporta esses casos como drifted e sai com exit 2
(mesma semântica de "stale, reaudite à mão" que o
codeql-reapply-dismissals.sh já usa), com cobertura em
scripts/security/tests/test_rekey_ledger.py.
§5. Empirical validation
§5.1 — Persistência do dismissal entre re-análises (2026-05-01)
Hypothesis: dismissals via REST API persistem entre re-análises CodeQL
no mesmo repositório; o state=dismissed não é resetado quando o workflow
codeql.yml re-roda na mesma branch (ou após merge para main).
Procedure:
- Aplicar dismissal apenas no alerta #43 (
credentials.rs:49,dismissed_reason=false_positive) na branchsecurity/gar-491-codeql-suppressions-2026-05-01. - Imediato:
gh api repos/michelbr84/GarraRUST/code-scanning/alerts/43 --jq '{state,dismissed_reason}'deve retornar{"state":"dismissed", "dismissed_reason":"false_positive"}. - Push commit no-op (esta seção §5 atualizada com run IDs) na branch para
acionar
codeql.ymlre-run. - Aguardar CodeQL run completar (~16 min, baseline conhecido do PR #106).
- Re-query o alerta —
statedeve continuardismissed.
Result (preenchido após CodeQL re-run completar em
security/gar-491-codeql-suppressions-2026-05-01 — última atualização
2026-05-01):
| Step | Run ID | Timestamp (UTC) | Status |
|---|---|---|---|
| Pre-dismissal CodeQL on main (baseline) | 25202502297 | 2026-05-01T04:39:43Z | success |
| Initial dismissal of #43 (PATCH) | n/a | 2026-05-01T12:33:36Z | success — state=dismissed, reason="false positive", by michelbr84 |
| Verify state immediate (gh api re-query) | n/a | 2026-05-01T12:33:38Z | success — confirmed dismissed |
Push commit 34b155b, trigger CodeQL re-run on branch | 25214464719 | 2026-05-01T12:35:44Z (start) | success |
| Re-query #43 post-rerun | n/a | 2026-05-01T~12:43Z | state=dismissed PERSISTED — dismissed_reason="false positive", dismissed_at=2026-05-01T12:33:36Z (unchanged) |
Apply remaining 5 dismissals via --apply | n/a | 2026-05-01T~12:44Z | success — 5 applied, 0 errors |
| Final verification: all 6 dismissed | n/a | 2026-05-01T~12:45Z | success — all 6 {state:"dismissed"} |
Verdict: ✅ Empirical proof PASSED. The REST-dismissal mechanism
preserves state=dismissed across CodeQL re-analysis of the same branch.
Mechanism approved for the batch.
Final state of all 6 alerts:
{"n":40,"reason":"used in tests","state":"dismissed"}
{"n":41,"reason":"used in tests","state":"dismissed"}
{"n":42,"reason":"used in tests","state":"dismissed"}
{"n":43,"reason":"false positive","state":"dismissed"}
{"n":44,"reason":"used in tests","state":"dismissed"}
{"n":45,"reason":"used in tests","state":"dismissed"}
Idempotency: confirmed empirically — a second --apply run on the
same ledger reports 6 skipped, 0 applied, 0 errors. The script's
fail-closed validation (rule_id + path + start_line) re-passes for each
entry, and the API-form-aware skip check correctly identifies
already-dismissed alerts.
§5.2 — O -test não apaga alerta já dispensado (2026-08-29)
Previsão que foi falsificada. §1 afirmava que as 5 entradas de fixture
(mobile_auth.rs, validation.rs) ficariam stale — exit 3 — assim que o
CODEQL_EXTRACTOR_RUST_OPTION_CARGO_CFG_OVERRIDES: "-test" chegasse à main.
O raciocínio era: sem extração, sem alerta.
Medição. Com o -test já em main desde c3e7521 (PR #869, 13:29Z), o
workflow codeql-apply-dismissals.yml rodou em --dry-run sem --alert,
percorrendo as 23 entradas:
| Run | Inputs | Resultado |
|---|---|---|
33261274173 | apply:false, alert:165 | DRY-RUN: would PATCH alert #165 state=dismissed reason='won't fix' — applied: 0, dry-run: 1, errors: 0 |
33261314173 | apply:true, alert:165 | applied: 1, skipped: 0, errors: 0 |
33261350845 | apply:true, alert:165 | skip: already dismissed (reason='won't fix') — idempotência confirmada |
33261527354 | apply:false (sem escopo) | skipped: 23 (already-dismissed), applied: 0, errors: 0 |
Verdict: ❌ previsão falsa. Nenhuma das 5 entradas ficou stale. A
transição para state=fixed acontece para alertas que estavam open e
deixaram de aparecer na análise; um alerta já dismissed permanece dismissed
independentemente de o código continuar sendo extraído. O -test reduz alertas
novos, não retroage sobre dispensados.
Consequência prática. A armadilha descrita em §2 — "o loop aborta na primeira
entrada stale, então uma obsoleta no topo impediria as de baixo" — é uma
propriedade real do script, mas não se materializou aqui: a run sem escopo
chegou até o #165. O input alert continua útil como bisturi, não como
contorno obrigatório.
Ganho colateral, que vale registrar. A validação fail-closed (rule_id +
path + start_line) roda antes do skip de já-dispensado
(codeql-reapply-dismissals.sh:184-200 vs :229). Como a run fechou com
errors: 0 e sem exit 2, ela provou mecanicamente que as 23 entradas ainda
apontam para o mesmo rule/path/linha que o ledger registra. Isso cobre a
metade mecânica do re-audit de 90 dias exigido por §3 regra 4 — não a metade de
julgamento (a justificativa continua fazendo sentido?), que segue pendente. Ver
o aviso de expiração no topo deste arquivo.
§5.3 — Os 16 path-injection foram RENUMERADOS, não reabertos (2026-08-29)
Medido pelo codeql-triage.yml em ba6d86b
(run 33263640687),
severity=high, state=open: 24 alertas, e 16 deles são
rust/path-injection nos mesmos arquivos e linhas que este ledger já dispensa.
Não é regressão nem o dismissal falhando. §5.2 provou que #67–#82 continuam
dismissed. O que aconteceu é que a re-análise pós-upgrade do bundle
(2.26.3 → 2.26.4) emitiu um segundo conjunto de alertas para os mesmos
achados, com números novos. O mapeamento é um-para-um por rule_id + path +
linha:
| Ledger (dismissed) | Live (open) | Arquivo:linha |
|---|---|---|
| #67 | #149 | skins_handler.rs:84 |
| #68 | #147 | skins_handler.rs:111 |
| #69 | #148 | skins_handler.rs:141 |
| #76 | #161 | skins_handler.rs:104 |
| #77 | #162 | skins_handler.rs:134 |
| #70 | #150 | skills_handler.rs:177 |
| #71 | #153 | skills_handler.rs:269 |
| #72 | #154 | skills_handler.rs:344 |
| #73 | #151 | skills_handler.rs:533 |
| #74 | #152 | skills_handler.rs:590 |
| #75 | #155 | skills_handler.rs:632 |
| #78 | #156 | skills_handler.rs:167 |
| #79 | #157 | skills_handler.rs:227 |
| #80 | #158 | skills_handler.rs:312 |
| #81 | #159 | skills_handler.rs:523 |
| #82 | #160 | skills_handler.rs:579 |
Por que o alert_number NÃO foi atualizado agora. Duas razões, nenhuma
delas burocrática:
- As linhas vão mudar de novo. O merge de
#874 (Copilot Autofix
para o #161) inseriu ~25 linhas em
skins_handler.rs::get_skin, e as linhas dos sinks pinados já se moveram no fonte::104 → :136,:134 → :159,:141 → :166,:111 → :117. Só:84(#67/#149) não mexeu. Renumerar antes da próxima análise do CodeQL sobreba6d86bproduziria um ledger que nasce stale. - O autofix pode ter resolvido #161 de verdade — ele adicionou
canonicalize+ checagem de prefixo emget_skin, que é exatamente o que o CodeQL queria. Se resolveu, a entrada correspondente sai do ledger em vez de ser renumerada.
Próximo passo, na ordem certa: esperar a análise CodeQL de ba6d86b
concluir → rodar codeql-apply-dismissals.yml com apply:false sem escopo
→ ler o exit 2/exit 3 que ele der → reconciliar .json e .md num PR
revisável. §3 regra 5 é explícita: divergência não se auto-corrige.
Os outros 8 High, todos verificados no fonte e todos falso-positivo ou material de ledger, nenhum exigindo código:
| Regra | # | Local | Leitura |
|---|---|---|---|
rust/cleartext-logging | #111, #112 | config_cmd.rs:213,236 | Imprime "set"/"not set" e a lista de nomes de provider com chave — nunca o valor. É o invariante de redaction do plan 0046. |
rust/cleartext-logging | #113 | wizard/mod.rs:640 | Imprime o nome da entrada do cofre; o key vai só para vault.set(entry, key). |
rust/cleartext-transmission | #143–#146 | signal/mod.rs:143,203, whatsapp/api.rs:48,79 | Ainda não auditados linha a linha. |
rust/cleartext-storage-database | #115 | session_store.rs:1261 | Ainda não auditado. |
Correção de escopo: o plano anterior falava em "33 sites de non-https-url".
Essa regra não aparece entre os alertas abertos. A onda High real é
16 + 4 + 3 + 1 = 24.
§6. Failure handling (no global filter fallback)
Se a empirical proof §5 falhar (state reverte para open após CodeQL
re-run):
- PARAR — não aplicar os 5 dismissals restantes.
- DOCUMENTAR aqui em §5 com run IDs e timestamps do failure.
- ABRIR sub-issue Linear
GAR-491.Xcom o problema empírico observado. - NÃO silenciar globalmente via
query-filters: exclude(proibido por §3 rule 6). - PR #1 fica em draft permanente até nova decisão.
Decisões aceitáveis para nova sub-issue:
- Custom query suite
.qlscom predicates Rust customizados (alta granularidade, alto custo de manutenção). - Path-specific approach a definir caso a caso.
- Manual UI dismissal mantendo este ledger versionado (admite que o script de reaplicação não é confiável; revogação via UI vira fonte de verdade).
§7. Reapply automation
Script: scripts/security/codeql-reapply-dismissals.sh
Funcionalidades:
--dry-run(default em CI; mostra o que seria reaplicado sem PATCH).--apply(oposto explícito; faz PATCH).--check-md(valida que.md↔.jsonlistam os mesmosalert_number).--alert <N>(escopo a um único alerta — usado pela empirical proof).
Fail-closed: para cada entry, antes de PATCH, o script confirma que o
alerta atual em GitHub tem mesmo rule_id, path, line que o ledger.
Se divergir → exit 2 + diagnóstico, manual re-audit obrigatório.
Sem schedule automático nesta PR (per amendment A8). Decisão de
agendamento fica em sub-issue follow-up GAR-491.2 quando o mecanismo
estiver provado e estável.
§8. See also
docs/security/codeql-setup.md— runbook do advanced setup, contexto histórico, paths-ignore.docs/security/dependabot-status.md— sister ledger para Dependabot residuals..github/codeql-config.yml—paths-ignore(não usado para suppression; só para autobuild safety)..github/workflows/codeql.yml— workflow advanced.- Linear: GAR-486 (umbrella), GAR-491 (this), GAR-490 (Wave 1, blocked-by 491).