security: harden trading pipeline and fail-closed risk controls #159
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # .github/workflows/security.yml | |
| name: "Security & Supply Chain" | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| schedule: | |
| - cron: "0 6 * * 1" # Weekly Monday 06:00 UTC | |
| # Principle of Least Privilege — deny-by-default | |
| permissions: {} | |
| env: | |
| CARGO_TERM_COLOR: always | |
| RUSTFLAGS: "-D warnings" | |
| CARGO_INCREMENTAL: 0 | |
| jobs: | |
| # ── 1. cargo-deny: licenses, bans, advisories, sources ───── | |
| cargo-deny: | |
| name: "cargo-deny (licenses + bans + advisories)" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| - uses: EmbarkStudios/cargo-deny-action@91bf2b620e09e18d6eb78b92e7861937469acedb # v2.0.17 | |
| with: | |
| command: check | |
| arguments: --all-features | |
| # ── 2. cargo-audit: RustSec vulnerability scan ───────────── | |
| cargo-audit: | |
| name: "cargo-audit (CVE scan)" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - name: Install and run cargo-audit | |
| run: | | |
| cargo install cargo-audit --locked | |
| # Ignore rustls-webpki CVEs from transitive dep jsonschema→reqwest@0.11→rustls-webpki@0.101 | |
| # These do NOT affect actual TLS connections (which use rustls-webpki@0.103.13) | |
| # TODO: Migrate jsonschema 0.17 → 0.46 to eliminate this transitive chain | |
| cargo audit \ | |
| --ignore RUSTSEC-2026-0098 \ | |
| --ignore RUSTSEC-2026-0099 \ | |
| --ignore RUSTSEC-2026-0104 | |
| # ── 3. cargo-vet: first-party dependency verification ────── | |
| cargo-vet: | |
| name: "cargo-vet (dependency verification)" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - name: Install cargo-vet | |
| run: cargo install --locked cargo-vet | |
| - name: Initialize cargo-vet (if first run) | |
| run: | | |
| if [ ! -d "supply-chain" ]; then | |
| cargo vet init | |
| cargo vet regenerate exemptions | |
| fi | |
| - name: Run cargo-vet | |
| run: cargo vet --locked | |
| # ── 4. Clippy (strict lint + deny unsafe) ────────────────── | |
| clippy: | |
| name: "Clippy (strict lints)" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: clippy | |
| - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 | |
| - name: Clippy (all features, all targets) | |
| run: | | |
| cargo clippy --workspace --all-features --all-targets -- \ | |
| -D warnings \ | |
| -D clippy::all \ | |
| -D clippy::pedantic \ | |
| -D clippy::cargo \ | |
| -A clippy::module_name_repetitions \ | |
| -A clippy::must_use_candidate \ | |
| -A clippy::cast_precision_loss \ | |
| -A clippy::cast_possible_truncation \ | |
| -A clippy::cast_sign_loss \ | |
| -A clippy::cast_lossless \ | |
| -A clippy::cast_possible_wrap \ | |
| -A clippy::uninlined_format_args \ | |
| -A clippy::doc_markdown \ | |
| -A clippy::missing_errors_doc \ | |
| -A clippy::missing_panics_doc \ | |
| -A clippy::unreadable_literal \ | |
| -A clippy::too_many_lines \ | |
| -A clippy::too_many_arguments \ | |
| -A clippy::struct_field_names \ | |
| -A clippy::needless_pass_by_value \ | |
| -A clippy::redundant_closure_for_method_calls \ | |
| -A clippy::inline_always \ | |
| -A clippy::return_self_not_must_use \ | |
| -A clippy::new_without_default \ | |
| -A clippy::upper_case_acronyms \ | |
| -A clippy::many_single_char_names \ | |
| -A clippy::multiple_crate_versions \ | |
| -A clippy::trivially_copy_pass_by_ref \ | |
| -A clippy::unused_async \ | |
| -A clippy::semicolon_if_nothing_returned \ | |
| -A clippy::cloned_instead_of_copied \ | |
| -A clippy::collapsible_else_if \ | |
| -A clippy::collapsible_match \ | |
| -A clippy::default_trait_access \ | |
| -A clippy::explicit_iter_loop \ | |
| -A clippy::ignore_without_reason \ | |
| -A clippy::manual_is_multiple_of \ | |
| -A clippy::manual_midpoint \ | |
| -A clippy::manual_range_contains \ | |
| -A clippy::map_unwrap_or \ | |
| -A clippy::match_same_arms \ | |
| -A clippy::needless_borrow \ | |
| -A clippy::needless_continue \ | |
| -A clippy::needless_raw_string_hashes \ | |
| -A clippy::unused_enumerate_index \ | |
| -A clippy::vec_init_then_push \ | |
| -A clippy::manual_string_new \ | |
| -A clippy::approx_constant \ | |
| -A clippy::collapsible_str_replace \ | |
| -A clippy::derivable_impls \ | |
| -A clippy::doc_link_with_quotes \ | |
| -A clippy::duration_suboptimal_units \ | |
| -A clippy::float_cmp \ | |
| -A clippy::format_push_string \ | |
| -A clippy::implicit_clone \ | |
| -A clippy::manual_clamp \ | |
| -A clippy::manual_strip \ | |
| -A clippy::needless_borrows_for_generic_args \ | |
| -A clippy::never_loop \ | |
| -A clippy::no_effect_underscore_binding \ | |
| -A clippy::similar_names \ | |
| -A clippy::struct_excessive_bools \ | |
| -A clippy::unnecessary_literal_bound \ | |
| -A clippy::unnecessary_sort_by \ | |
| -A clippy::unnecessary_wraps \ | |
| -A clippy::unused_self \ | |
| -A clippy::used_underscore_binding \ | |
| -A clippy::useless_format \ | |
| -A clippy::wildcard_imports \ | |
| -A clippy::explicit_into_iter_loop \ | |
| -A clippy::assigning_clones \ | |
| -A clippy::elidable_lifetime_names \ | |
| -A clippy::filter_next \ | |
| -A clippy::if_not_else \ | |
| -A clippy::manual_let_else \ | |
| -A clippy::match_like_matches_macro \ | |
| -A clippy::needless_range_loop \ | |
| -A clippy::unnested_or_patterns \ | |
| -A clippy::items_after_test_module \ | |
| -A clippy::match_wildcard_for_single_variants \ | |
| -A clippy::should_implement_trait \ | |
| -A clippy::explicit_auto_deref | |
| # ── 5. Format check ──────────────────────────────────────── | |
| fmt: | |
| name: "rustfmt" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| - uses: dtolnay/rust-toolchain@nightly | |
| with: | |
| components: rustfmt | |
| - run: cargo fmt --all -- --check | |
| # ── 6. cargo-auditable build (SBOM in binary) ───────────── | |
| auditable-build: | |
| name: "cargo-auditable (SBOM-embedded binary)" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 | |
| - name: Install cargo-auditable & cargo-audit | |
| run: cargo install --locked cargo-auditable cargo-audit | |
| - name: Build with embedded dependency info | |
| run: cargo auditable build --workspace --release | |
| - name: Scan compiled binaries for vulnerabilities | |
| run: | | |
| for bin in target/release/daemon target/release/tui target/release/cli; do | |
| if [ -f "$bin" ]; then | |
| echo "Scanning $bin..." | |
| cargo audit bin "$bin" || true | |
| fi | |
| done | |
| # ── 7. Ensure SHA-pinned actions ─────────────────────────── | |
| pin-check: | |
| name: "Verify all Actions are SHA-pinned" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| - uses: zgosalvez/github-actions-ensure-sha-pinned-actions@ca46236c6ce584ae24bc6283ba8dcf4b3ec8a066 # v5.0.4 | |
| with: | |
| allowlist: | | |
| actions/* | |
| dtolnay/* | |
| dtolnay/rust-toolchain | |
| # ── 8. Verify #![forbid(unsafe_code)] in all crates ─────── | |
| unsafe-check: | |
| name: "Verify forbid(unsafe_code)" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| - name: "Verify #![forbid(unsafe_code)] in all crates" | |
| run: | | |
| MISSING="" | |
| for lib in crates/*/src/lib.rs; do | |
| # Skip vendored third-party crates that legitimately use unsafe | |
| case "$lib" in | |
| *curve25519-dalek-patch*) continue ;; | |
| esac | |
| if ! grep -q 'forbid(unsafe_code)' "$lib"; then | |
| MISSING="$MISSING\n MISSING: $lib" | |
| fi | |
| done | |
| if [ -n "$MISSING" ]; then | |
| echo -e "ERROR: Missing #![forbid(unsafe_code)] in:$MISSING" | |
| exit 1 | |
| fi | |
| echo "OK: All crates have #![forbid(unsafe_code)]" |