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
52 changes: 21 additions & 31 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@ jobs:
- name: Test the update manifest builder
run: scripts/test-build-update-manifest.sh

# The security gate has to tell "nothing found" from "could not look":
# a missing jq, no report, or a report that does not parse once read as
# a pass (#729).
- name: Test the cargo-audit gate
run: scripts/test-cargo-audit-check.sh

# ARCHITECTURE section 5 is the reference for the Tauri command surface, and
# it had drifted into describing ten commands that were never written — a
# reader following it wrote calls that fail at runtime with "command not
Expand Down Expand Up @@ -228,17 +234,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Read pinned Rust version
id: rust-toolchain-file
# bash explicitly: the default shell on a Windows runner is PowerShell,
# where `$(...)` and `$GITHUB_OUTPUT` mean nothing, so the step passes
# having written no output — and the toolchain action then fails with
# "toolchain is a required input", eight steps from the real cause.
# rustup ships on GitHub-hosted runners and reads rust-toolchain.toml
# itself, so the version and components live in that one file and no
# third-party action sits between it and the build.
- name: Install Rust toolchain
shell: bash
run: echo "channel=$(sed -n 's/^channel *= *"\(.*\)"/\1/p' rust-toolchain.toml)" >> "$GITHUB_OUTPUT"
- uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de # v1
with:
toolchain: ${{ steps.rust-toolchain-file.outputs.channel }}
run: rustup toolchain install --profile minimal --no-self-update
- name: Install jq
run: sudo apt-get install -y jq
- name: Install cargo-audit
Expand Down Expand Up @@ -294,18 +295,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Read pinned Rust version
id: rust-toolchain-file
# bash explicitly: the default shell on a Windows runner is PowerShell,
# where `$(...)` and `$GITHUB_OUTPUT` mean nothing, so the step passes
# having written no output — and the toolchain action then fails with
# "toolchain is a required input", eight steps from the real cause.
# rustup ships on GitHub-hosted runners and reads rust-toolchain.toml
# itself, so the version and components live in that one file and no
# third-party action sits between it and the build.
- name: Install Rust toolchain
shell: bash
run: echo "channel=$(sed -n 's/^channel *= *"\(.*\)"/\1/p' rust-toolchain.toml)" >> "$GITHUB_OUTPUT"
- uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de # v1
with:
toolchain: ${{ steps.rust-toolchain-file.outputs.channel }}
components: rustfmt, clippy
run: rustup toolchain install --profile minimal --no-self-update
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
workspaces: src-tauri
Expand Down Expand Up @@ -340,17 +335,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Read pinned Rust version
id: rust-toolchain-file
# bash explicitly: the default shell on a Windows runner is PowerShell,
# where `$(...)` and `$GITHUB_OUTPUT` mean nothing, so the step passes
# having written no output — and the toolchain action then fails with
# "toolchain is a required input", eight steps from the real cause.
# rustup ships on GitHub-hosted runners and reads rust-toolchain.toml
# itself, so the version and components live in that one file and no
# third-party action sits between it and the build.
- name: Install Rust toolchain
shell: bash
run: echo "channel=$(sed -n 's/^channel *= *"\(.*\)"/\1/p' rust-toolchain.toml)" >> "$GITHUB_OUTPUT"
- uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de # v1
with:
toolchain: ${{ steps.rust-toolchain-file.outputs.channel }}
run: rustup toolchain install --profile minimal --no-self-update
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
workspaces: src-tauri
Expand Down
39 changes: 14 additions & 25 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,12 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Read pinned Rust version
id: rust-toolchain-file
# bash explicitly: the default shell on a Windows runner is PowerShell,
# where `$(...)` and `$GITHUB_OUTPUT` mean nothing, so the step passes
# having written no output — and the toolchain action then fails with
# "toolchain is a required input", eight steps from the real cause.
# rustup ships on GitHub-hosted runners and reads rust-toolchain.toml
# itself, so the version and components live in that one file and no
# third-party action sits between it and the build.
- name: Install Rust toolchain
shell: bash
run: echo "channel=$(sed -n 's/^channel *= *"\(.*\)"/\1/p' rust-toolchain.toml)" >> "$GITHUB_OUTPUT"
- uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de # v1
with:
toolchain: ${{ steps.rust-toolchain-file.outputs.channel }}
run: rustup toolchain install --profile minimal --no-self-update
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
workspaces: src-tauri
Expand Down Expand Up @@ -181,22 +176,16 @@ jobs:
node-version-file: ".node-version"
cache: "npm"

- name: Read pinned Rust version
id: rust-toolchain-file
# bash explicitly: the default shell on a Windows runner is PowerShell,
# where `$(...)` and `$GITHUB_OUTPUT` mean nothing, so the step passes
# having written no output — and the toolchain action then fails with
# "toolchain is a required input", eight steps from the real cause.
shell: bash
run: echo "channel=$(sed -n 's/^channel *= *"\(.*\)"/\1/p' rust-toolchain.toml)" >> "$GITHUB_OUTPUT"

# From rust-toolchain.toml, so releases build on the same version CI
# tests against. rustup ships on GitHub-hosted runners and reads the
# file itself; bash, because the Windows default shell is PowerShell.
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de # v1
with:
# From rust-toolchain.toml, so releases build on the same version
# CI tests against.
toolchain: ${{ steps.rust-toolchain-file.outputs.channel }}
targets: ${{ matrix.rust_target }}
shell: bash
env:
RUST_TARGET: ${{ matrix.rust_target }}
run: |
rustup toolchain install --profile minimal --no-self-update
rustup target add "$RUST_TARGET"

- name: Rust cache
uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
Expand Down
1 change: 1 addition & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ lint-workflows:
actionlint -no-color -oneline
./scripts/check-action-pins.sh
./scripts/test-check-action-pins.sh
./scripts/test-cargo-audit-check.sh

# Format Rust and everything dprint owns.
fmt:
Expand Down
4 changes: 2 additions & 2 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
# contributor runs before `just <task>`.
#
# CI does NOT depend on mise — it reads .node-version and rust-toolchain.toml
# directly via actions/setup-node and dtolnay/rust-toolchain, so those two
# files stay the source of truth and nothing here can drift from them.
# directly via actions/setup-node and rustup, so those two files stay the
# source of truth and nothing here can drift from them.
#
# Note: mise can read those idiomatic files itself, but that is off by
# default (idiomatic_version_file_enable_tools), so the versions are
Expand Down
2 changes: 1 addition & 1 deletion rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Single source of truth for the Rust toolchain.
#
# Read natively by rustup, by dtolnay/rust-toolchain in CI, and by mise.
# Read natively by rustup (locally and in CI) and by mise.
# Pinned to an exact version rather than "stable": SQLPilot is an
# application, not a published library, so a reproducible build matters
# more than a wide MSRV — and an unpinned toolchain means a new stable
Expand Down
30 changes: 28 additions & 2 deletions scripts/cargo-audit-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,30 @@ if ! echo "$JSON_OUTPUT" | jq -e 'type == "object" and has("vulnerabilities")' >
exit 2
fi

# Say how fresh the advisory database is. cargo audit fetches it before each
# run, but a --no-fetch run or `fetch = false` in audit.toml answers from
# whatever copy is on disk, and an old copy reports clean.
DB_UPDATED="$(echo "$JSON_OUTPUT" | jq -r '.database."last-updated" // empty')"
if [ -z "$DB_UPDATED" ]; then
echo "::warning::cargo-audit-check: the report does not say when the advisory database was last updated." >&2
elif DB_EPOCH="$(date -d "$DB_UPDATED" +%s 2>/dev/null)"; then
DB_AGE_DAYS=$(( ($(date +%s) - DB_EPOCH) / 86400 ))
echo "Advisory database last updated $DB_UPDATED ($DB_AGE_DAYS day(s) ago)."
if [ "$DB_AGE_DAYS" -gt 7 ]; then
echo "::warning::cargo-audit-check: the advisory database is $DB_AGE_DAYS days old, so newer advisories are not checked. Run without --no-fetch to update it." >&2
fi
else
# BSD date (macOS) has no -d; show the timestamp and let the reader judge.
echo "Advisory database last updated $DB_UPDATED."
fi
echo ""

# Pretty-print the full advisory list to job logs (visible, not hidden).
echo "=== cargo audit findings ==="

echo "$JSON_OUTPUT" | jq -r '
# Parse first, then print: a parse failure must stop the gate, while the
# display pipeline below may legitimately end early (head closing the pipe).
if ! FINDINGS="$(echo "$JSON_OUTPUT" | jq -r '
(.vulnerabilities.list // []) as $vulns |
(.warnings.unmaintained // []) as $unm |
(.warnings.unsound // []) as $uns |
Expand All @@ -96,7 +116,13 @@ echo "$JSON_OUTPUT" | jq -r '
(.advisory.package // "?"),
.advisory.title
] | @tsv
' | column -t -s $'\t' 2>/dev/null | head -50 || echo "(parse failed)"
')"; then
echo "::error::cargo-audit-check: could not parse the cargo audit report; refusing to call this a pass." >&2
exit 2
fi
if [ -n "$FINDINGS" ]; then
printf '%s\n' "$FINDINGS" | column -t -s $'\t' 2>/dev/null | head -50 || true
fi

# Always show the known-accepted list — even after they clear from
# cargo audit output, the list serves as a reminder of past accepted.
Expand Down
124 changes: 124 additions & 0 deletions scripts/test-cargo-audit-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#!/usr/bin/env bash
# scripts/test-cargo-audit-check.sh
#
# Tests for cargo-audit-check.sh.
#
# The gate's job is to tell "nothing found" from "could not look" (#729), so
# most cases here are ways of not looking: a missing tool, no report, a report
# that does not parse, a stale advisory database. A fake `cargo` on PATH plays
# back a canned `cargo audit --json` report per case, so no network or real
# advisory database is needed.

set -euo pipefail

script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
checker="$script_dir/cargo-audit-check.sh"
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT

passed=0
failed=0
ok() { printf ' ok %s\n' "$1"; passed=$((passed + 1)); }
bad() { printf ' FAIL %s\n %s\n' "$1" "$2"; failed=$((failed + 1)); }

# A bin directory holding every tool on the real PATH except jq, so the
# "jq is missing" case can be run without uninstalling anything.
nojq_bin="$work/nojq-bin"
mkdir -p "$nojq_bin"
IFS=: read -r -a path_dirs <<< "$PATH"
for dir in "${path_dirs[@]}"; do
[[ -d "$dir" ]] || continue
for tool in "$dir"/*; do
name="${tool##*/}"
if [[ "$name" != jq && ! -e "$nojq_bin/$name" && -x "$tool" ]]; then
ln -s "$tool" "$nojq_bin/$name"
fi
done
done

# Fakes for cargo and cargo-audit: `cargo audit --json` prints $REPORT_FILE.
fake_bin="$work/fake-bin"
mkdir -p "$fake_bin"
cat > "$fake_bin/cargo" <<'EOF'
#!/usr/bin/env bash
[[ "${1:-}" == audit ]] || { echo "fake cargo: only 'audit' is faked" >&2; exit 99; }
cat "$REPORT_FILE"
exit "${AUDIT_EXIT:-0}"
EOF
printf '#!/usr/bin/env bash\nexit 0\n' > "$fake_bin/cargo-audit"
chmod +x "$fake_bin/cargo" "$fake_bin/cargo-audit"

# Run the checker against report `$2`, with `$3` as the base PATH.
run_case() {
local name="$1" report="$2" base_path="${3:-$PATH}"
local dir="$work/$name"
mkdir -p "$dir/ws"
printf '%s' "$report" > "$dir/report.json"
(
cd "$dir"
unset GITHUB_STEP_SUMMARY
REPORT_FILE="$dir/report.json" CARGO_WORKSPACE_DIR=ws \
PATH="$fake_bin:$base_path" bash "$checker" 2>&1
)
}

expect_status() {
local name="$1" want="$2" report="$3" needle="$4" base_path="${5:-$PATH}" output status
output=$(run_case "$name" "$report" "$base_path") && status=0 || status=$?
if [[ $status -ne $want ]]; then
bad "$name" "expected exit $want, got $status: $output"
elif [[ "$output" != *"$needle"* ]]; then
bad "$name" "expected the output to mention '$needle', got: $output"
else
ok "$name"
fi
}

today="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
old="2020-01-01T00:00:00Z"

report() { # report <last-updated> <vulnerabilities list> <unmaintained list>
printf '{"database":{"last-updated":"%s"},"vulnerabilities":{"found":false,"count":0,"list":%s},"warnings":{"unmaintained":%s}}' \
"$1" "$2" "$3"
}
advisory() { # advisory <id>
printf '{"advisory":{"id":"%s","package":"pkg","title":"a title"}}' "$1"
}

many_warnings="[$(for i in $(seq 1 80); do advisory "RUSTSEC-0000-$i"; if [[ $i -lt 80 ]]; then printf ','; fi; done)]"

echo "cargo-audit-check.sh"

expect_status "a clean report passes" 0 \
"$(report "$today" '[]' '[]')" "0 real vulnerabilities"

expect_status "a real vulnerability fails" 1 \
"$(report "$today" "[$(advisory RUSTSEC-2099-0001)]" '[]')" "RUSTSEC-2099-0001"

expect_status "warnings alone do not fail" 0 \
"$(report "$today" '[]' "[$(advisory RUSTSEC-2099-0002)]")" "1 non-blocking"

expect_status "more findings than the display shows still passes" 0 \
"$(report "$today" '[]' "$many_warnings")" "80 non-blocking"

expect_status "jq missing fails" 2 \
"$(report "$today" '[]' '[]')" "'jq' is not installed" "$nojq_bin"

expect_status "no report fails" 2 \
"" "did not produce a report"

expect_status "a report that is not JSON fails" 2 \
"error: failed to fetch advisory database" "did not produce a report"

expect_status "a report whose findings do not parse fails" 2 \
"$(report "$today" '"not-a-list"' '[]')" "could not parse"

expect_status "a stale advisory database warns" 0 \
"$(report "$old" '[]' '[]')" "days old"

expect_status "the database age is shown" 0 \
"$(report "$today" '[]' '[]')" "0 day(s) ago"

echo ""
echo "$passed passed, $failed failed"
[[ $failed -eq 0 ]]
Loading