Skip to content

Commit 7d771ca

Browse files
committed
Tooling: harden checker against supply-chain attacks
- Pass `--locked` to every operational cargo command in checks (`cargo clippy`, `cargo nextest run` in both unit and integration tests, `cargo +nightly udeps`). Without it cargo silently re-resolves `Cargo.lock` on metadata drift, opening a window for a fresh malicious version to land mid-build across 1028 transitive crates. - Pin `apps/desktop/rust-toolchain.toml` channel from floating `stable` to `1.95.0`. - Pin every tool install. `cargo install` now passes `--version` + `--locked` for `cargo-audit`, `cargo-deny`, `cargo-machete`, `cargo-udeps`, `cargo-nextest`. Every `EnsureGoTool` call replaced `@latest` with a specific version: `staticcheck@v0.7.0`, `nilaway@v0.0.0-20260515015210-fd187751154f`, `misspell@v0.3.4`, `gocyclo@v0.6.0`, `ineffassign@v0.2.0`, `deadcode@v0.45.0`. Closes the wave-1-2-class "the supply-chain tool itself gets trojaned" gap. - New `workflows-hardening` check at `scripts/check/checks/desktop-workflows-hardening.go`. Scans `.github/workflows/*.{yml,yaml}` and fails on three classes the wave-4 (TanStack, May 2026) attack chained: tag/branch-pinned third-party actions (must be SHA-pinned, with `./...` local actions exempt), `pull_request_target` triggers, and workflow-scoped `id-token: write` (must be job-scoped). Cmdr passes today; the check is a regression guard. ~190 lines + ~200 lines of tests. - New `govulncheck` check at `scripts/check/checks/scripts-go-govulncheck.go`. Runs `govulncheck@v1.3.0` against every `go.mod`. Mirrors `cargo-audit`'s role on the Rust side: static-analysis-based, low false-positive rate. - Bump `.mise.toml` go from `1.25.7` to `1.25.10`. The `govulncheck` check found 7 reachable stdlib vulns in cmdr's tooling on its first run (CVE fixes in `net`, `crypto/tls`, `crypto/x509`, `net/url`, `archive/tar`, `os`). 1.25.10 is the latest patch in the 1.25 line. - Update `scripts/check/CLAUDE.md` with four new Decision entries (the `--locked` rule, the tool-version-pin rule, the workflow-hardening check, the govulncheck check) and the apps/checks table now lists `govulncheck` and the new Security row. The Rust hardening is mostly defense in depth; cmdr already has `cargo-audit`, `cargo-deny` (licenses/bans/sources), SHA-pinned actions, and `pnpm install --frozen-lockfile` in every CI workflow. The two new checks close the gaps surfaced in our pre-launch supply-chain review: no Go-side vuln scanner, no regression guard on the workflow hardening that's already in place.
1 parent f01af35 commit 7d771ca

20 files changed

Lines changed: 525 additions & 20 deletions

.mise.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tools]
22
node = "25"
33
pnpm = "11.0.9"
4-
go = "1.25.7"
4+
go = "1.25.10"
55
# Rust is managed by rustup via rust-toolchain.toml

apps/desktop/rust-toolchain.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
[toolchain]
2-
channel = "stable"
2+
# Pinned to a specific stable version, not the floating "stable" channel.
3+
# A compromised rustc release would land transparently otherwise. Bump
4+
# deliberately, with a few days between Rust's release and our pin update.
5+
channel = "1.95.0"

scripts/check/CLAUDE.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,9 @@ The chosen `RUST_LOG` value is echoed at the top of the timestamped log so it's
147147
captured. When unset, the log starts with `=== RUST_LOG unset (default warn level) ===`.
148148

149149
**Go tool auto-install:** `EnsureGoTool(name, installPath)` checks PATH first, then runs `go install` and returns the
150-
full binary path. Used for staticcheck, nilaway, etc.
150+
full binary path. Used for staticcheck, nilaway, etc. The `installPath` MUST pin a specific version (`@vX.Y.Z` or a
151+
pseudo-version), never `@latest`: an `@latest` install is the Go-side equivalent of the wave-1-2 npm-registry-trojan
152+
vector. Same rule applies to `cargo install` calls inside checks: pin both `--version` and `--locked`.
151153

152154
**TTY detection:** `golang.org/x/term.IsTerminal` gates the live status line; CI logs stay clean.
153155

@@ -223,8 +225,9 @@ before tests.
223225
| Website | Astro | prettier, eslint, typecheck, build, html-validate, e2e |
224226
| Website | Docker | docker-build |
225227
| API server | TS | oxfmt, eslint, typecheck, tests |
226-
| Scripts | Go | gofmt, go-vet, staticcheck, ineffassign, misspell, gocyclo, nilaway, deadcode, go-tests |
228+
| Scripts | Go | gofmt, go-vet, staticcheck, ineffassign, misspell, gocyclo, nilaway, deadcode, go-tests, govulncheck |
227229
| Other | Metrics | file-length (warn-only), CLAUDE.md-reminder (warn-only), changelog-commit-links |
230+
| Other | Security | workflows-hardening (SHA-pinning, no `pull_request_target`, job-scoped `id-token: write`) |
228231

229232
## Output format
230233

@@ -260,6 +263,33 @@ dependencies (gtk3-rs, unic-\*, fxhash, proc-macro-error, etc.) trigger unmainta
260263
`cargo-audit` still catches critical security vulnerabilities. License, bans, and sources checks in `cargo-deny` remain
261264
active. See comment in `src-tauri/deny.toml`.
262265

266+
**Decision**: every operational `cargo` command in checks passes `--locked`. **Why**: without it, cargo silently
267+
re-resolves `Cargo.lock` whenever upstream metadata shifts (a yank, a new transitive dep version). For a 1028-crate
268+
lockfile, that resolution window is wide and lets a freshly-published malicious version land mid-build. `--locked`
269+
fails loudly if the lockfile would change. Applies to `cargo clippy`, `cargo nextest run` (in both `desktop-rust-tests`
270+
and `desktop-rust-integration-tests`), and `cargo +nightly udeps`. Audit/deny/machete read `Cargo.lock` without
271+
updating it, so `--locked` is moot for them, but the install of those tools still uses `--locked` to lock the tool's
272+
own dep tree.
273+
274+
**Decision**: every tool install pins `--version` and `--locked` (cargo) or `@vX.Y.Z` (Go). **Why**: an unpinned tool
275+
install (`cargo install cargo-audit` or `EnsureGoTool(..., "@latest")`) means each fresh checkout pulls whatever's
276+
latest. A wave-1-2-class compromise of any of these tool repositories would auto-propagate. Pinning is the Go-side
277+
equivalent of the pnpm `minimum-release-age` defense (a fresh version can't land without a deliberate bump).
278+
279+
**Decision**: `workflows-hardening` check enforces three GitHub Actions invariants and acts as a regression guard.
280+
**Why**: cmdr's workflows are already correctly hardened (every third-party action is SHA-pinned with a comment, no
281+
`pull_request_target` triggers, no workflow-scoped `id-token: write`). Without an automated guard, a future PR or a
282+
Renovate misconfiguration could silently regress any of those without anyone noticing in review. The check fails on
283+
tag/branch-pinned third-party actions, on `pull_request_target` triggers (wave-4's entry vector), and on
284+
workflow-scoped `id-token: write` (must be job-scoped per the wave-4 OIDC-token-extraction lesson). Local actions
285+
(`./...`) are exempt.
286+
287+
**Decision**: `govulncheck` runs against every Go module. **Why**: cargo-audit covers Rust deps; nothing covered Go
288+
until now. `govulncheck` is static-analysis-based, so it only flags vulns actually reachable from the code (low false
289+
positive rate). Most of cmdr's Go modules are dep-free tooling scripts but still call into the Go stdlib, which gets
290+
its own CVEs; the check found 7 real reachable stdlib vulns the first time it ran (fixed by bumping mise's Go pin).
291+
Mirrors the cargo-audit role on the Rust side.
292+
263293
**Decision**: `cfg-gate` check to catch ungated macOS-only crate imports. **Why**: Rust code using macOS-only crates
264294
(from `[target.'cfg(target_os = "macos")'.dependencies]`) compiles fine on macOS but fails on Linux if the `use` isn't
265295
wrapped in `#[cfg(target_os = "macos")]`. CI catches this after push, but the check catches it locally and instantly. It

scripts/check/checks/desktop-rust-cargo-audit.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ func parseAuditJSON(output string) (cargoAuditReport, error) {
143143
// RunCargoAudit checks for security vulnerabilities.
144144
func RunCargoAudit(ctx *CheckContext) (CheckResult, error) {
145145
if !CommandExists("cargo-audit") {
146-
installCmd := exec.Command("cargo", "install", "cargo-audit")
146+
installCmd := exec.Command("cargo", "install", "cargo-audit", "--version", "0.22.1", "--locked")
147147
if _, err := RunCommand(installCmd, true); err != nil {
148148
return CheckResult{}, fmt.Errorf("failed to install cargo-audit: %w", err)
149149
}

scripts/check/checks/desktop-rust-cargo-deny.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func RunCargoDeny(ctx *CheckContext) (CheckResult, error) {
1919

2020
// Check if cargo-deny is installed
2121
if !CommandExists("cargo-deny") {
22-
installCmd := exec.Command("cargo", "install", "cargo-deny")
22+
installCmd := exec.Command("cargo", "install", "cargo-deny", "--version", "0.19.6", "--locked")
2323
if _, err := RunCommand(installCmd, true); err != nil {
2424
return CheckResult{}, fmt.Errorf("failed to install cargo-deny: %w", err)
2525
}

scripts/check/checks/desktop-rust-cargo-machete.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func RunCargoMachete(ctx *CheckContext) (CheckResult, error) {
1919
rustDir := filepath.Join(ctx.RootDir, "apps", "desktop", "src-tauri")
2020

2121
if !CommandExists("cargo-machete") {
22-
installCmd := exec.Command("cargo", "install", "cargo-machete", "--locked")
22+
installCmd := exec.Command("cargo", "install", "cargo-machete", "--version", "0.9.2", "--locked")
2323
if output, err := RunCommand(installCmd, true); err != nil {
2424
return CheckResult{}, fmt.Errorf("failed to install cargo-machete\n%s", indentOutput(output))
2525
}

scripts/check/checks/desktop-rust-cargo-udeps.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ func RunCargoUdeps(ctx *CheckContext) (CheckResult, error) {
2121

2222
// Check if cargo-udeps is installed
2323
if !CommandExists("cargo-udeps") {
24-
installCmd := exec.Command("cargo", "install", "cargo-udeps", "--locked")
24+
installCmd := exec.Command("cargo", "install", "cargo-udeps", "--version", "0.1.61", "--locked")
2525
if _, err := RunCommand(installCmd, true); err != nil {
2626
return CheckResult{}, fmt.Errorf("failed to install cargo-udeps: %w", err)
2727
}
2828
}
2929

3030
// cargo-udeps requires nightly
31-
cmd := exec.Command("cargo", "+nightly", "udeps", "--all-targets")
31+
cmd := exec.Command("cargo", "+nightly", "udeps", "--locked", "--all-targets")
3232
cmd.Dir = rustDir
3333
output, err := RunCommand(cmd, true)
3434
if err != nil {

scripts/check/checks/desktop-rust-clippy.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func RunClippy(ctx *CheckContext) (CheckResult, error) {
2828
// the only build pass we do. --fix is reserved for the failure branch
2929
// because it ignores -D warnings, can rewrite source files, and re-running
3030
// it speculatively doubled wall time on every clean run.
31-
cmd := exec.Command("cargo", "clippy", "--all-targets", "--", "-D", "warnings")
31+
cmd := exec.Command("cargo", "clippy", "--locked", "--all-targets", "--", "-D", "warnings")
3232
cmd.Dir = rustDir
3333
output, err := RunCommand(cmd, true)
3434
if err != nil {
@@ -37,7 +37,7 @@ func RunClippy(ctx *CheckContext) (CheckResult, error) {
3737
}
3838

3939
// Locally: try to auto-fix, then re-check.
40-
fixCmd := exec.Command("cargo", "clippy", "--all-targets", "--fix", "--allow-dirty", "--allow-staged")
40+
fixCmd := exec.Command("cargo", "clippy", "--locked", "--all-targets", "--fix", "--allow-dirty", "--allow-staged")
4141
fixCmd.Dir = rustDir
4242
_, _ = RunCommand(fixCmd, true)
4343

scripts/check/checks/desktop-rust-integration-tests.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func RunRustIntegrationTests(ctx *CheckContext) (CheckResult, error) {
6060

6161
// Make sure cargo-nextest is available (mirrors desktop-rust-tests.go).
6262
if !CommandExists("cargo-nextest") {
63-
installCmd := exec.Command("cargo", "install", "cargo-nextest", "--locked")
63+
installCmd := exec.Command("cargo", "install", "cargo-nextest", "--version", "0.9.136", "--locked")
6464
if _, err := RunCommand(installCmd, true); err != nil {
6565
return CheckResult{}, fmt.Errorf("failed to install cargo-nextest: %w", err)
6666
}
@@ -72,6 +72,7 @@ func RunRustIntegrationTests(ctx *CheckContext) (CheckResult, error) {
7272
// tests are still skipped.
7373
cmd := exec.Command(
7474
"cargo", "nextest", "run",
75+
"--locked",
7576
"--release",
7677
"--run-ignored", "only",
7778
"-E", "test(smb_integration_)",

scripts/check/checks/desktop-rust-tests.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ func RunRustTests(ctx *CheckContext) (CheckResult, error) {
1414

1515
// Check if cargo-nextest is installed
1616
if !CommandExists("cargo-nextest") {
17-
installCmd := exec.Command("cargo", "install", "cargo-nextest", "--locked")
17+
installCmd := exec.Command("cargo", "install", "cargo-nextest", "--version", "0.9.136", "--locked")
1818
if _, err := RunCommand(installCmd, true); err != nil {
1919
return CheckResult{}, fmt.Errorf("failed to install cargo-nextest: %w", err)
2020
}
2121
}
2222

23-
cmd := exec.Command("cargo", "nextest", "run")
23+
cmd := exec.Command("cargo", "nextest", "run", "--locked")
2424
cmd.Dir = rustDir
2525
output, err := RunCommand(cmd, true)
2626
if err != nil {

0 commit comments

Comments
 (0)