Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .mise.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tools]
node = "25"
pnpm = "11.0.9"
go = "1.25.7"
go = "1.25.10"
# Rust is managed by rustup via rust-toolchain.toml
5 changes: 4 additions & 1 deletion apps/desktop/rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
[toolchain]
channel = "stable"
# Pinned to a specific stable version, not the floating "stable" channel.
# A compromised rustc release would land transparently otherwise. Bump
# deliberately, with a few days between Rust's release and our pin update.
channel = "1.95.0"
34 changes: 32 additions & 2 deletions scripts/check/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ The chosen `RUST_LOG` value is echoed at the top of the timestamped log so it's
captured. When unset, the log starts with `=== RUST_LOG unset (default warn level) ===`.

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

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

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

## Output format

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

**Decision**: every operational `cargo` command in checks passes `--locked`. **Why**: without it, cargo silently
re-resolves `Cargo.lock` whenever upstream metadata shifts (a yank, a new transitive dep version). For a 1028-crate
lockfile, that resolution window is wide and lets a freshly-published malicious version land mid-build. `--locked`
fails loudly if the lockfile would change. Applies to `cargo clippy`, `cargo nextest run` (in both `desktop-rust-tests`
and `desktop-rust-integration-tests`), and `cargo +nightly udeps`. Audit/deny/machete read `Cargo.lock` without
updating it, so `--locked` is moot for them, but the install of those tools still uses `--locked` to lock the tool's
own dep tree.

**Decision**: every tool install pins `--version` and `--locked` (cargo) or `@vX.Y.Z` (Go). **Why**: an unpinned tool
install (`cargo install cargo-audit` or `EnsureGoTool(..., "@latest")`) means each fresh checkout pulls whatever's
latest. A wave-1-2-class compromise of any of these tool repositories would auto-propagate. Pinning is the Go-side
equivalent of the pnpm `minimum-release-age` defense (a fresh version can't land without a deliberate bump).

**Decision**: `workflows-hardening` check enforces three GitHub Actions invariants and acts as a regression guard.
**Why**: cmdr's workflows are already correctly hardened (every third-party action is SHA-pinned with a comment, no
`pull_request_target` triggers, no workflow-scoped `id-token: write`). Without an automated guard, a future PR or a
Renovate misconfiguration could silently regress any of those without anyone noticing in review. The check fails on
tag/branch-pinned third-party actions, on `pull_request_target` triggers (wave-4's entry vector), and on
workflow-scoped `id-token: write` (must be job-scoped per the wave-4 OIDC-token-extraction lesson). Local actions
(`./...`) are exempt.

**Decision**: `govulncheck` runs against every Go module. **Why**: cargo-audit covers Rust deps; nothing covered Go
until now. `govulncheck` is static-analysis-based, so it only flags vulns actually reachable from the code (low false
positive rate). Most of cmdr's Go modules are dep-free tooling scripts but still call into the Go stdlib, which gets
its own CVEs; the check found 7 real reachable stdlib vulns the first time it ran (fixed by bumping mise's Go pin).
Mirrors the cargo-audit role on the Rust side.

**Decision**: `cfg-gate` check to catch ungated macOS-only crate imports. **Why**: Rust code using macOS-only crates
(from `[target.'cfg(target_os = "macos")'.dependencies]`) compiles fine on macOS but fails on Linux if the `use` isn't
wrapped in `#[cfg(target_os = "macos")]`. CI catches this after push, but the check catches it locally and instantly. It
Expand Down
2 changes: 1 addition & 1 deletion scripts/check/checks/desktop-rust-cargo-audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ func parseAuditJSON(output string) (cargoAuditReport, error) {
// RunCargoAudit checks for security vulnerabilities.
func RunCargoAudit(ctx *CheckContext) (CheckResult, error) {
if !CommandExists("cargo-audit") {
installCmd := exec.Command("cargo", "install", "cargo-audit")
installCmd := exec.Command("cargo", "install", "cargo-audit", "--version", "0.22.1", "--locked")
if _, err := RunCommand(installCmd, true); err != nil {
return CheckResult{}, fmt.Errorf("failed to install cargo-audit: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion scripts/check/checks/desktop-rust-cargo-deny.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func RunCargoDeny(ctx *CheckContext) (CheckResult, error) {

// Check if cargo-deny is installed
if !CommandExists("cargo-deny") {
installCmd := exec.Command("cargo", "install", "cargo-deny")
installCmd := exec.Command("cargo", "install", "cargo-deny", "--version", "0.19.6", "--locked")
if _, err := RunCommand(installCmd, true); err != nil {
return CheckResult{}, fmt.Errorf("failed to install cargo-deny: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion scripts/check/checks/desktop-rust-cargo-machete.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func RunCargoMachete(ctx *CheckContext) (CheckResult, error) {
rustDir := filepath.Join(ctx.RootDir, "apps", "desktop", "src-tauri")

if !CommandExists("cargo-machete") {
installCmd := exec.Command("cargo", "install", "cargo-machete", "--locked")
installCmd := exec.Command("cargo", "install", "cargo-machete", "--version", "0.9.2", "--locked")
if output, err := RunCommand(installCmd, true); err != nil {
return CheckResult{}, fmt.Errorf("failed to install cargo-machete\n%s", indentOutput(output))
}
Expand Down
4 changes: 2 additions & 2 deletions scripts/check/checks/desktop-rust-cargo-udeps.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ func RunCargoUdeps(ctx *CheckContext) (CheckResult, error) {

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

// cargo-udeps requires nightly
cmd := exec.Command("cargo", "+nightly", "udeps", "--all-targets")
cmd := exec.Command("cargo", "+nightly", "udeps", "--locked", "--all-targets")
cmd.Dir = rustDir
output, err := RunCommand(cmd, true)
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions scripts/check/checks/desktop-rust-clippy.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func RunClippy(ctx *CheckContext) (CheckResult, error) {
// the only build pass we do. --fix is reserved for the failure branch
// because it ignores -D warnings, can rewrite source files, and re-running
// it speculatively doubled wall time on every clean run.
cmd := exec.Command("cargo", "clippy", "--all-targets", "--", "-D", "warnings")
cmd := exec.Command("cargo", "clippy", "--locked", "--all-targets", "--", "-D", "warnings")
cmd.Dir = rustDir
output, err := RunCommand(cmd, true)
if err != nil {
Expand All @@ -37,7 +37,7 @@ func RunClippy(ctx *CheckContext) (CheckResult, error) {
}

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

Expand Down
3 changes: 2 additions & 1 deletion scripts/check/checks/desktop-rust-integration-tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func RunRustIntegrationTests(ctx *CheckContext) (CheckResult, error) {

// Make sure cargo-nextest is available (mirrors desktop-rust-tests.go).
if !CommandExists("cargo-nextest") {
installCmd := exec.Command("cargo", "install", "cargo-nextest", "--locked")
installCmd := exec.Command("cargo", "install", "cargo-nextest", "--version", "0.9.136", "--locked")
if _, err := RunCommand(installCmd, true); err != nil {
return CheckResult{}, fmt.Errorf("failed to install cargo-nextest: %w", err)
}
Expand All @@ -72,6 +72,7 @@ func RunRustIntegrationTests(ctx *CheckContext) (CheckResult, error) {
// tests are still skipped.
cmd := exec.Command(
"cargo", "nextest", "run",
"--locked",
"--release",
"--run-ignored", "only",
"-E", "test(smb_integration_)",
Expand Down
4 changes: 2 additions & 2 deletions scripts/check/checks/desktop-rust-tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ func RunRustTests(ctx *CheckContext) (CheckResult, error) {

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

cmd := exec.Command("cargo", "nextest", "run")
cmd := exec.Command("cargo", "nextest", "run", "--locked")
cmd.Dir = rustDir
output, err := RunCommand(cmd, true)
if err != nil {
Expand Down
192 changes: 192 additions & 0 deletions scripts/check/checks/desktop-workflows-hardening.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package checks

import (
"bufio"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)

// RunWorkflowsHardening enforces three GitHub Actions invariants that protect
// against the wave-4 (TanStack, May 2026) class of supply-chain attack:
//
// 1. Every third-party action `uses:` reference must be SHA-pinned (40 hex
// chars), not tag- or branch-pinned. Tag refs are mutable; a malicious
// re-tag would silently land. Local `./...` actions and reusable
// workflows that resolve to the same repo are exempt.
//
// 2. No workflow may use the `pull_request_target` trigger. It runs in the
// base repo's security context with access to the cache scope and
// GITHUB_TOKEN — the exact entry vector that let attacker code poison
// TanStack's pnpm store and steal the OIDC token.
//
// 3. `id-token: write` must be job-scoped, never workflow-scoped. A
// workflow-level grant means every job in the workflow can mint OIDC
// tokens; if any one of them runs attacker-controlled code, the token
// can be exfiltrated. Job-scoping isolates the privilege to the single
// publishing step.
//
// All three classes are silent in normal review: tag pins look identical to
// SHA pins, `pull_request_target` looks like a typo of `pull_request`, and
// permissions blocks are usually skimmed. The check makes them loud.
func RunWorkflowsHardening(ctx *CheckContext) (CheckResult, error) {
wfDir := filepath.Join(ctx.RootDir, ".github", "workflows")
entries, err := os.ReadDir(wfDir)
if err != nil {
if os.IsNotExist(err) {
return Skipped("no .github/workflows/"), nil
}
return CheckResult{}, fmt.Errorf("failed to read workflows dir: %w", err)
}

var files []string
for _, e := range entries {
if e.IsDir() {
continue
}
name := e.Name()
if strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml") {
files = append(files, filepath.Join(wfDir, name))
}
}
sort.Strings(files)

var violations []string
scanned := 0
for _, f := range files {
v, err := scanWorkflowFile(f, ctx.RootDir)
if err != nil {
return CheckResult{}, err
}
violations = append(violations, v...)
scanned++
}

if len(violations) > 0 {
return CheckResult{}, fmt.Errorf("workflow hardening violations\n%s",
indentOutput(strings.Join(violations, "\n")))
}

result := Success(fmt.Sprintf("%d %s, all hardened", scanned, Pluralize(scanned, "workflow", "workflows")))
result.Total = scanned
return result, nil
}

var (
// uses: <owner>/<repo>@<ref> (optionally followed by space + comment)
// Captures: 1=owner/repo[/subpath], 2=ref
usesRefRE = regexp.MustCompile(`^(\s*)(?:-\s+)?uses:\s+([^@\s]+)@([^\s#]+)`)

// 40-char hex SHA — the only acceptable ref form for third-party actions.
sha40RE = regexp.MustCompile(`^[a-f0-9]{40}$`)

// `id-token: write` line. We care about its indentation level relative
// to the containing job to tell workflow-scope from job-scope.
idTokenWriteRE = regexp.MustCompile(`^(\s*)id-token:\s*write\s*$`)

// Top-level `on:` block introducer.
onIntroRE = regexp.MustCompile(`^on:\s*$`)
// Inline `on:` form like `on: [push, pull_request_target]` or `on: pull_request_target`.
onInlineRE = regexp.MustCompile(`^on:\s+(.+)$`)
// A trigger key under `on:`, like ` pull_request_target:` (any indent).
triggerKeyRE = regexp.MustCompile(`^(\s+)([a-z_]+):`)
)

func scanWorkflowFile(path, repoRoot string) ([]string, error) {
rel, _ := filepath.Rel(repoRoot, path)
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("open %s: %w", rel, err)
}
defer f.Close()

var (
violations []string
inOnBlock bool
onIndent int = -1
lineNum int
)
scanner := bufio.NewScanner(f)
for scanner.Scan() {
lineNum++
line := scanner.Text()
trimmed := strings.TrimRight(line, " \t")
if trimmed == "" || strings.HasPrefix(strings.TrimSpace(trimmed), "#") {
continue
}

// (1) tag-pinned actions
if m := usesRefRE.FindStringSubmatch(trimmed); m != nil {
repo, ref := m[2], m[3]
if !isExemptUsesRef(repo) && !sha40RE.MatchString(ref) {
violations = append(violations,
fmt.Sprintf("%s:%d: tag/branch-pinned action: %s@%s (SHA-pin: '@<40-hex> # %s')",
rel, lineNum, repo, ref, ref))
}
}

// (2) pull_request_target trigger
if onIntroRE.MatchString(trimmed) {
inOnBlock = true
onIndent = -1
continue
}
if m := onInlineRE.FindStringSubmatch(trimmed); m != nil {
if strings.Contains(m[1], "pull_request_target") {
violations = append(violations,
fmt.Sprintf("%s:%d: pull_request_target trigger (wave-4 entry vector)", rel, lineNum))
}
continue
}
if inOnBlock {
if m := triggerKeyRE.FindStringSubmatch(line); m != nil {
indent := len(m[1])
if onIndent == -1 {
onIndent = indent
}
if indent == onIndent {
if m[2] == "pull_request_target" {
violations = append(violations,
fmt.Sprintf("%s:%d: pull_request_target trigger (wave-4 entry vector)", rel, lineNum))
}
continue
}
}
// Any line at indent <= 0 (i.e. a new top-level key) ends the on: block.
if !strings.HasPrefix(line, " ") && !strings.HasPrefix(line, "\t") && trimmed != "" {
inOnBlock = false
onIndent = -1
}
}

// (3) workflow-scoped id-token: write
// Workflow-level `permissions:` is at column 0 (top-level key), so
// its children sit at 2-space indent. Job-level `permissions:` lives
// under `jobs.<name>.`, so its children sit at ≥6-space indent. We
// flag anything with indent ≤ 4 as workflow-scoped.
if m := idTokenWriteRE.FindStringSubmatch(line); m != nil {
indent := len(m[1])
if indent <= 4 {
violations = append(violations,
fmt.Sprintf("%s:%d: workflow-scoped 'id-token: write' (must be job-scoped)", rel, lineNum))
}
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("read %s: %w", rel, err)
}
return violations, nil
}

// isExemptUsesRef returns true for `uses:` targets that don't need SHA pinning:
// local action paths (./...) and own-repo references (where the SHA pin would
// be pointless since the action and the workflow are versioned together).
func isExemptUsesRef(repo string) bool {
if strings.HasPrefix(repo, "./") || strings.HasPrefix(repo, "../") {
return true
}
return false
}
Loading
Loading