CodeQL Setup Runbook
Status: established 2026-04-30 (PR C of the Green Security Baseline sprint, plan
personal-api-key-revogada-vectorized-matsumoto§Step 2). Linear: GAR-XXX umbrella, sub-issue 3. Scope: how GarraRUST runs CodeQL static analysis, why we use advanced setup, and the one-time toggle procedure required to migrate from GitHub-native default setup.
Background
Until 2026-04-30, GarraRUST relied on GitHub-native default setup for CodeQL. Default setup is convenient — no workflow file, GitHub manages language detection, autobuild, and scheduling — but it has two dealbreakers for this repo:
- Autobuild fails on
crates/garraia-desktop(Tauri). The crate'sbuild.rsdepends on the WebView2 SDK on Windows and GTK/glib on Linux. GitHub-hosted runners don't have these by default. The "Code scanning configuration error" banner in the Security tab tracked back to this autobuild failure, not to a real analysis problem. - No path-level exclusion control. Default setup scans the entire workspace. Excluding the desktop crate, the bench PoC, and the Playwright E2E fixtures is not configurable through the UI.
Advanced setup — a checked-in .github/workflows/codeql.yml plus
.github/codeql-config.yml — solves both via two complementary
mechanisms:
build-mode: none(buildless extraction). CodeQL for Rust does NOT supportbuild-mode: manual(verified empirically against run25176031230on the first attempt of this workflow:"Rust does not support the manual build mode. Please try using one of the following build modes instead: none."). Buildless Rust extraction means we do NOT need to runcargo build— the analyzer reads source files directly. This eliminates the autobuild surface that broke default setup.- Explicit
paths-ignorein the config file so analysis still excludescrates/garraia-desktop,apps/garraia-mobile,benches, andtests/playwright— same exclusion listci.yml/cargo-audit.yml/mutants.ymlalready use.
What this PR adds
.github/workflows/codeql.yml— advanced workflow. Two language jobs (rustandjavascript-typescript, both withbuild-mode: none) onubuntu-latest, triggered on push/PR tomain+ weekly Monday 09:00 UTC schedule. Action versions matchci.yml:actions/checkout@v6,github/codeql-action/init@v3+analyze@v3. (No Rust toolchain install needed — buildless extraction reads sources directly.).github/codeql-config.yml—paths-ignoreforcrates/garraia-desktop/**,apps/garraia-mobile/**,benches/**,tests/playwright/**. Mirrors the exclusion set used byci.yml,cargo-audit.yml, andmutants.yml.- This runbook.
One-time toggle: disable default setup BEFORE merging this PR
GitHub does not allow advanced setup and default setup to coexist silently. If both are active, SARIF uploads collide under the same category and one of them errors out.
Procedure (must be done in GitHub UI; gh API supports the same endpoint but the user should explicitly authorize this destructive toggle):
- Open
https://github.com/michelbr84/GarraRUST/settings/security_analysis. - Scroll to Code scanning → Default setup.
- Click Disable. Confirm.
- Verify state via API:
gh api repos/michelbr84/GarraRUST/code-scanning/default-setup \--jq '.state'# expected: not-configured
- Merge this PR. The new workflow runs on the merge commit and on
every subsequent push/PR to
main.
API alternative (only if the user explicitly authorizes the gh CLI to disable default setup):
gh api -X PATCH repos/michelbr84/GarraRUST/code-scanning/default-setup \
-f state=not-configured
This is reversible: if advanced setup misbehaves, default setup can be re-enabled the same way.
Why these specific exclusions
| Path | Why excluded |
|---|---|
crates/garraia-desktop/** | Tauri. Build requires WebView2 / GTK absent from GitHub-hosted runners. Already excluded from ci.yml, cargo-audit.yml, mutants.yml. Local-only build via scripts/build-installer.ps1. |
apps/garraia-mobile/** | Flutter. CodeQL JS/TS would only see Dart-generated artifacts, which are out of scope. |
benches/** | PoC bench harness, ephemeral per CLAUDE.md. Has its own [workspace] and would confuse CodeQL build resolution. |
tests/playwright/** | Playwright TypeScript fixtures — scanned by their own runner. CodeQL JS/TS focuses on admin UI source. |
What we did NOT change
- The 90 existing CodeQL alerts are NOT triaged in this PR. Triage waves
are tracked separately as Linear sub-issues
GAR-490(Wave 1, production paths: ~16 path-injection inskills_handler.rs/skins_handler.rs, 8 sql-injection ingroups.rs/invites.rs) andGAR-491(Wave 2, test fixtures + suppression convention). - AMENDMENT 2026-05-01 (GAR-491): suppression mechanism for Rust CodeQL
alerts has now been decided. Rust CodeQL still does NOT support inline
// codeql[...]comments (PR github/codeql#21638 is open without merge). The chosen mechanism is REST API dismissal + a versioned ledger — seedocs/security/codeql-suppressions.mdfor the human-readable ledger anddocs/security/codeql-suppressions.jsonfor the machine-readable source consumed byscripts/security/codeql-reapply-dismissals.sh. Wave 2 (GAR-491) entrega a convenção + script + 6 dismissals individualmente justificados; a empirical proof (persistência do dismissal decredentials.rs:49entre re-análises CodeQL) é o gate obrigatório antes do batch dos 5 restantes. Sem fallback global: se a prova falhar, abort + nova decisão (semquery-filters: excludepor rule-id). - The
query_suitedefaults todefault(was the same in default setup). Switching toextendedorsecurity-extendedis a separate decision that surfaces more alerts; not appropriate while we still have 90 unresolved.
Verification after merge
# 1. Workflow ran at least once
gh run list --workflow=codeql.yml --limit 3
# 2. CodeQL analyses succeeded with no error string
gh api repos/michelbr84/GarraRUST/code-scanning/analyses \
--jq '.[0:3] | .[] | {ref, tool: .tool.name, error, results_count}'
# 3. "Code scanning configuration error" banner no longer appears in
# the GitHub Security tab.
# 4. Default setup is off
gh api repos/michelbr84/GarraRUST/code-scanning/default-setup \
--jq '.state'
# expected: not-configured
Triage planning (next sessions)
GAR-XXX.4 and GAR-XXX.5 carry the actual alert resolution. Wave 1
prioritizes production code paths; Wave 2 covers test fixtures and
locks in the suppression convention. Both reference the alert numbers
captured in the Security tab and avoid bulk-dismissal anti-patterns.
A onda de 2026-08-28: extractor Rust e escopo de teste
Em 2026-08-28 o Security tab passou a mostrar 56 alertas Critical de uma vez,
sem nenhuma mudança em codeql.yml, em codeql-config.yml ou de código de
segurança na janela.
Causa: o bundle do CodeQL nos runners subiu de 2.26.3 para 2.26.4, e isso
destravou o extractor de Rust. Comparando os logs do job Analyze (rust) do
mesmo workflow, na mesma branch:
Run (main) | CodeQL | Extraídos com erro | Extraídos sem erro |
|---|---|---|---|
32170604886 — 2026-08-18T18:22Z | 2.26.3 | 303 | 118 |
33233416706 — 2026-08-29T04:16Z | 2.26.4 | 3 | 422 |
O log novo diz CodeQL scanned 425 out of 425 Rust files. A cobertura saiu de
~28% para ~99% — 3,6× mais código analisado. Os alertas não foram regressão: era
código que já existia e que o CodeQL passou a enxergar.
Por que quase tudo era código de teste
204 dos 434 arquivos .rs de crates/ têm um módulo #[cfg(test)] inline,
e o extractor Rust liga cfg(test) incondicionalmente — no fonte do
github/codeql, rust/extractor/src/config.rs::to_cfg_overrides faz
enabled_cfgs.insert(to_cfg_override("test")) sem condição. Todo fixture de
teste (senha literal, salt, chave de HMAC) virou sink analisável de uma vez.
Das 78 ocorrências de rust/hard-coded-cryptographic-value (security-severity
9.8, Critical) no workspace, 69 eram código de teste: 59 em #[cfg(test)]
inline e 10 em crates/*/tests/.
A correção: escopo, não supressão
.github/workflows/codeql.yml, no nível do job analyze:
jobs:
analyze:
env:
CODEQL_EXTRACTOR_RUST_OPTION_CARGO_CFG_OVERRIDES: "-test"
O nível importa, e essa foi a primeira tentativa errada. Colocar a env no step
Initialize CodeQLnão funciona. Combuild-mode: nonea extração acontece dentro dePerform CodeQL analysis— 811 s no run33233416706, contra 10 s do init — então uma env no escopo do init nunca chega ao extractor. O sintoma foi um alerta apontando para umassert!dentro de um#[cfg(test)]no run33239057983, com o log mostrando 339 arquivos extraídos: os 85 decrates/*/tests/tinham sumido (opaths-ignorefuncionou) mas os módulos inline continuavam lá.
Opção oficial, documentada em rust/codeql-extractor.yml do github/codeql:
"Comma-separated list of cfg settings to enable, or disable if prefixed with
-." Complementada por crates/*/tests/** no paths-ignore do
codeql-config.yml, que cobre os testes de integração em arquivos próprios (o
-test sozinho não os remove, porque não são gated por cfg(test)).
Isto não é supressão: não toca em codeql-suppressions.json, não faz
PATCH state=dismissed, e fica versionado e revisável no diff. Resolve
exatamente o bloqueio registrado em
codeql-suppressions.md §1 em 2026-05 — "paths-ignore
silencia arquivo inteiro; os testes do GarraRUST são INLINE dentro de produção".
Helpers atrás de #[cfg(feature = "test-helpers")] continuam analisados: são
feature, não cfg(test).
Como validar em runs futuros
No log do job Analyze (rust), comparar contra a baseline de
33233416706 (425/425, 3 com erro):
CodeQL scanned N out of M Rust files
| Total number of Rust files that were extracted with errors | ... |
| Total number of Rust files that were extracted without error | ... |
Se a cobertura de produção cair, reverter — o objetivo é remover teste, não perder alcance.
Triagem programática
GET /repos/{owner}/{repo}/code-scanning/alerts exige o escopo
security_events, que tokens de PAT/integração normalmente não têm — foi
exatamente o que impediu diagnosticar a onda de 2026-08-28 pela API. O
GITHUB_TOKEN de um job com permissions: security-events: read tem.
.github/workflows/codeql-triage.yml
(workflow_dispatch) roda
scripts/security/codeql-alert-report.py
e publica:
- no job summary, uma tabela
rule_id | severidade | total | produção | teste; - como artifact,
codeql-alerts.json(lista crua) ecodeql-alerts.md.
A classificação produção-vs-teste é heurística — caminho de teste, ou linha do
alerta dentro de um #[cfg(test)] — e serve para dimensionar uma onda, não para
dispensar triagem individual.
O sentinela all
Os três inputs (severity, state, tool) aceitam all para dizer "todos".
Use-o em vez de tentar passar string vazia: o Actions substitui string vazia pelo
default: declarado no input (critical / open / CodeQL), então o valor
"vazio = todos" é inalcançável via API — uma triagem antiga chegou a reportar
"0 alertas" que valia só para Critical por causa disso.
Para varrer o repositório inteiro sem nenhum filtro, o que responde "este alerta existe em algum estado?" numa execução só:
| input | valor |
|---|---|
severity | all |
state | all |
tool | all |
O script por baixo omite da query todo filtro vazio, então o sentinela só
traduz all → "" antes de chamá-lo.
Localmente, com um token que tenha o escopo (aqui o vazio funciona direto, porque
não há default: do Actions no caminho):
GITHUB_TOKEN=... python3 scripts/security/codeql-alert-report.py \
--repo michelbr84/GarraRUST --severity critical --state open
# todos os alertas, todos os estados, todas as ferramentas
GITHUB_TOKEN=... python3 scripts/security/codeql-alert-report.py \
--repo michelbr84/GarraRUST --severity "" --state "" --tool ""
See also
.github/workflows/codeql.yml— workflow definition..github/codeql-config.yml— paths-ignore..github/workflows/ci.yml— source of the matching exclusions (--exclude garraia-desktop).docs/security/secret-scanning-runbook.md— companion runbook for the secret-scanning side of the security baseline.docs/security/threat-model.md— overall security model.