Repository navigation
Security Deep #244
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
| name: Security Deep | |
| on: | |
| schedule: | |
| - cron: '0 20 * * *' # 20:00 UTC = 7 AM AEDT next day | |
| workflow_dispatch: # Allow manual triggers | |
| permissions: | |
| contents: read | |
| env: | |
| CARGO_TERM_COLOR: always | |
| RUST_BACKTRACE: 1 | |
| jobs: | |
| # Deep security analysis (< 2 hours) - nightly scheduled | |
| kani-verification: | |
| name: Kani Formal Verification | |
| runs-on: cachekit | |
| timeout-minutes: 30 | |
| env: | |
| # Pin rustup/cargo to /tmp (single fs) to avoid EXDEV on toolchain staging, | |
| # and so the kani-verifier binary lands in a known, PATH-able location. The | |
| # runner's default CARGO_HOME/bin is not on PATH, so cargo-kani exited 127. | |
| RUSTUP_HOME: /tmp/rustup | |
| CARGO_HOME: /tmp/cargo | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Put cargo bin on PATH | |
| run: echo "/tmp/cargo/bin" >> "$GITHUB_PATH" | |
| - name: Ensure stable toolchain | |
| run: | | |
| # Runner image ships stable; only install if missing. | |
| # Full `toolchain install` can hit EXDEV in containers (rust-lang/rustup#1239). | |
| rustup toolchain list | grep -q stable || rustup toolchain install stable | |
| - name: Install Kani | |
| run: | | |
| cargo +stable install --locked kani-verifier --version 0.67.0 | |
| cargo-kani setup | |
| - name: Run Kani verification | |
| run: | | |
| cd rust | |
| cargo-kani --tests --no-default-features --features compression,checksum,messagepack,encryption | |
| fuzzing: | |
| name: Extended Fuzzing (3 targets × 1h) | |
| runs-on: cachekit | |
| timeout-minutes: 200 | |
| env: | |
| # Avoid EXDEV "cross-device link" errors when rustup stages a nightly | |
| # toolchain across overlay/hostPath boundaries on the ARC runner pod | |
| # (rust-lang/rustup#1239). Same pattern as fuzz-smoke.yml. | |
| RUSTUP_HOME: /tmp/rustup | |
| CARGO_HOME: /tmp/cargo | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Install nightly Rust | |
| run: | | |
| # Pin nightly: cargo-fuzz 0.13.1 → rustix uses rustc_layout_scalar_valid_range_* | |
| # attributes reserved after nightly-2026-04-27. Last known-good date. | |
| # Same pin as fuzz-smoke.yml. | |
| rustup toolchain install nightly-2026-04-27 | |
| rustup default nightly-2026-04-27 | |
| - name: Install cargo-fuzz | |
| run: cargo install --locked cargo-fuzz | |
| - name: Fuzz byte_storage_compress (1 hour) | |
| run: | | |
| cd rust | |
| timeout 3600 cargo fuzz run byte_storage_compress --no-default-features --features compression,checksum || true | |
| - name: Fuzz byte_storage_decompress (1 hour) | |
| run: | | |
| cd rust | |
| timeout 3600 cargo fuzz run byte_storage_decompress --no-default-features --features compression,checksum || true | |
| # NOTE: encryption_roundtrip and 8 other encryption targets are stale against | |
| # cachekit-core 0.1.1 (encrypt_aes_gcm → encrypt_with_keys). See #114. Using | |
| # encryption_key_derivation, which compiles, until those targets are migrated. | |
| - name: Fuzz encryption_key_derivation (1 hour) | |
| run: | | |
| cd rust | |
| timeout 3600 cargo fuzz run encryption_key_derivation --no-default-features --features encryption || true | |
| - name: Check for crashes | |
| run: | | |
| cd rust/fuzz | |
| CRASHES=$(find artifacts -name "crash-*" 2>/dev/null | wc -l) | |
| if [ "$CRASHES" -gt 0 ]; then | |
| echo "❌ Found $CRASHES crashes during fuzzing" | |
| find artifacts -name "crash-*" -exec echo "Crash: {}" \; | |
| exit 1 | |
| fi | |
| echo "✅ No crashes found during fuzzing" | |
| - name: Generate coverage report | |
| run: | | |
| cd rust | |
| for target in byte_storage_compress byte_storage_decompress encryption_key_derivation; do | |
| echo "=== Coverage for $target ===" | |
| cargo fuzz coverage $target || true | |
| done | |
| atheris-fuzzing: | |
| name: Atheris Python-Rust Fuzzing | |
| runs-on: cachekit | |
| timeout-minutes: 60 | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| # Pin to 3.11: atheris 2.3.0's newest prebuilt wheel is cp311. On a newer | |
| # interpreter uv builds atheris from sdist, which needs clang + libFuzzer | |
| # (find_libfuzzer.sh) — absent on the self-hosted runner, so install fails | |
| # before any fuzzing runs. Both uv sync and uv run must pin, or uv run | |
| # re-resolves to a different interpreter and re-triggers the source build. | |
| - name: Install dependencies | |
| run: | | |
| uv sync --group dev --group fuzz --python 3.11 | |
| - name: Run Atheris fuzz targets (10 min each) | |
| run: | | |
| for fuzz_target in tests/fuzzing/fuzz_*.py; do | |
| if [ -f "$fuzz_target" ]; then | |
| echo "Fuzzing $fuzz_target..." | |
| timeout 10m uv run --python 3.11 python "$fuzz_target" -max_total_time=600 || true | |
| fi | |
| done | |
| - name: Upload crash corpus | |
| if: always() | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 | |
| with: | |
| name: atheris-crashes-${{ github.run_id }} | |
| path: tests/fuzzing/corpus/ | |
| - name: Report fuzzing results | |
| if: always() | |
| run: | | |
| if [ -d tests/fuzzing/corpus/ ] && find tests/fuzzing/corpus/ -mindepth 1 ! -name '.gitignore' -print -quit | grep -q .; then | |
| echo "⚠️ Crashes discovered during Atheris fuzzing!" | |
| ls -lh tests/fuzzing/corpus/ | |
| exit 1 | |
| fi | |
| echo "✅ No crashes discovered during Atheris fuzzing" | |
| miri-full: | |
| name: Miri Full Suite | |
| runs-on: cachekit | |
| timeout-minutes: 30 | |
| env: | |
| # Avoid EXDEV "cross-device link" errors when rustup stages a nightly | |
| # toolchain across overlay/hostPath boundaries on the ARC runner pod | |
| # (rust-lang/rustup#1239). Same pattern as fuzz-smoke.yml. | |
| RUSTUP_HOME: /tmp/rustup | |
| CARGO_HOME: /tmp/cargo | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Install nightly Rust with Miri | |
| run: | | |
| rustup toolchain install nightly --component miri | |
| rustup default nightly | |
| - name: Run full Miri test suite | |
| run: | | |
| cd rust | |
| cargo miri test --no-default-features --features compression,checksum,messagepack,encryption | |
| sanitizers: | |
| name: Sanitizers (ASan, TSan, MSan) | |
| runs-on: cachekit | |
| timeout-minutes: 40 | |
| env: | |
| # Avoid EXDEV "cross-device link" errors when rustup stages the nightly | |
| # toolchain / rust-src across overlay/hostPath boundaries on the ARC runner | |
| # pod (rust-lang/rustup#1239). Same pattern as fuzzing / miri-full jobs. | |
| RUSTUP_HOME: /tmp/rustup | |
| CARGO_HOME: /tmp/cargo | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| sanitizer: [address, thread, memory] | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Install nightly Rust | |
| run: | | |
| rustup toolchain install nightly | |
| rustup default nightly | |
| - name: Add source for sanitizers | |
| if: matrix.sanitizer == 'memory' || matrix.sanitizer == 'thread' | |
| run: rustup component add rust-src --toolchain nightly | |
| - name: Run AddressSanitizer | |
| if: matrix.sanitizer == 'address' | |
| env: | |
| RUSTFLAGS: -Zsanitizer=address | |
| run: | | |
| cd rust | |
| cargo +nightly test --lib --target x86_64-unknown-linux-gnu \ | |
| --no-default-features --features compression,checksum,messagepack,encryption | |
| - name: Run ThreadSanitizer | |
| if: matrix.sanitizer == 'thread' | |
| env: | |
| RUSTFLAGS: -Zsanitizer=thread | |
| TSAN_OPTIONS: suppressions=${{ github.workspace }}/rust/tsan_suppressions.txt | |
| run: | | |
| cd rust | |
| cargo +nightly test --lib --target x86_64-unknown-linux-gnu \ | |
| -Zbuild-std --no-default-features --features compression,checksum,messagepack,encryption | |
| - name: Run MemorySanitizer | |
| if: matrix.sanitizer == 'memory' | |
| env: | |
| RUSTFLAGS: -Zsanitizer=memory -Zsanitizer-memory-track-origins | |
| run: | | |
| cd rust | |
| cargo +nightly test --lib --target x86_64-unknown-linux-gnu \ | |
| -Zbuild-std --no-default-features --features compression,checksum,messagepack,encryption | |
| generate-security-report: | |
| name: Generate Security Report | |
| runs-on: cachekit | |
| needs: [kani-verification, fuzzing, atheris-fuzzing, miri-full, sanitizers] | |
| if: always() | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Generate security report | |
| run: | | |
| mkdir -p reports/security | |
| TIMESTAMP=$(date +%Y%m%d-%H%M%S) | |
| REPORT="reports/security/deep-analysis-$TIMESTAMP.md" | |
| cat > "$REPORT" <<EOF | |
| # Deep Security Analysis Report | |
| Generated: $(date -u +"%Y-%m-%d %H:%M:%S UTC") | |
| Workflow Run: ${{ github.run_id }} | |
| ## Summary | |
| - **Kani Verification**: ${{ needs.kani-verification.result }} | |
| - **Extended Fuzzing**: ${{ needs.fuzzing.result }} | |
| - **Atheris Python-Rust Fuzzing**: ${{ needs.atheris-fuzzing.result }} | |
| - **Miri Full Suite**: ${{ needs.miri-full.result }} | |
| - **Sanitizers**: ${{ needs.sanitizers.result }} | |
| ## Results | |
| ### Formal Verification (Kani) | |
| Status: ${{ needs.kani-verification.result }} | |
| - ByteStorage roundtrip properties | |
| - Checksum integrity verification | |
| - Decompression bomb protection | |
| - Encryption roundtrip correctness | |
| - Key derivation determinism | |
| ### Fuzzing (3 hours total) | |
| Status: ${{ needs.fuzzing.result }} | |
| - byte_storage_compress (1 hour) | |
| - byte_storage_decompress (1 hour) | |
| - encryption_key_derivation (1 hour) | |
| ### Undefined Behavior Detection (Miri) | |
| Status: ${{ needs.miri-full.result }} | |
| - Full test suite with strict provenance | |
| - Symbolic alignment checking | |
| - Memory leak detection | |
| ### Runtime Sanitizers | |
| Status: ${{ needs.sanitizers.result }} | |
| - AddressSanitizer (memory errors) | |
| - ThreadSanitizer (race conditions) | |
| - MemorySanitizer (uninitialized memory) | |
| ## Conclusion | |
| $(if [[ "${{ needs.kani-verification.result }}" == "success" ]] && \ | |
| [[ "${{ needs.fuzzing.result }}" == "success" ]] && \ | |
| [[ "${{ needs.miri-full.result }}" == "success" ]] && \ | |
| [[ "${{ needs.sanitizers.result }}" == "success" ]]; then | |
| echo "✅ All deep security checks passed" | |
| else | |
| echo "❌ One or more deep security checks failed" | |
| fi) | |
| EOF | |
| cat "$REPORT" | |
| - name: Archive security report | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 | |
| with: | |
| name: security-deep-report | |
| path: reports/security/ | |
| retention-days: 90 | |
| # Summary job | |
| security-deep-success: | |
| name: Security Deep Success | |
| runs-on: ubuntu-latest | |
| needs: [kani-verification, fuzzing, atheris-fuzzing, miri-full, sanitizers] | |
| if: always() | |
| permissions: | |
| contents: read | |
| issues: write | |
| steps: | |
| - name: Check all deep security checks passed | |
| run: | | |
| if [[ "${{ needs.kani-verification.result }}" != "success" ]] || \ | |
| [[ "${{ needs.fuzzing.result }}" != "success" ]] || \ | |
| [[ "${{ needs.atheris-fuzzing.result }}" != "success" ]] || \ | |
| [[ "${{ needs.miri-full.result }}" != "success" ]] || \ | |
| [[ "${{ needs.sanitizers.result }}" != "success" ]]; then | |
| echo "❌ One or more deep security checks failed" | |
| echo "Kani: ${{ needs.kani-verification.result }}" | |
| echo "Fuzzing: ${{ needs.fuzzing.result }}" | |
| echo "Atheris: ${{ needs.atheris-fuzzing.result }}" | |
| echo "Miri: ${{ needs.miri-full.result }}" | |
| echo "Sanitizers: ${{ needs.sanitizers.result }}" | |
| exit 1 | |
| fi | |
| echo "✅ All deep security checks passed" | |
| # Surface nightly failures: a schedule-only job that fails silently is worse | |
| # than no job. De-duplicate by title so we don't open a new issue every night. | |
| - name: File / update tracking issue on failure | |
| if: failure() | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TITLE: "Security Deep nightly is failing" | |
| run: | | |
| body=$(printf '%s\n' \ | |
| "Security Deep failed on run ${{ github.run_id }}." \ | |
| "" \ | |
| "- Kani: ${{ needs.kani-verification.result }}" \ | |
| "- Fuzzing: ${{ needs.fuzzing.result }}" \ | |
| "- Atheris: ${{ needs.atheris-fuzzing.result }}" \ | |
| "- Miri: ${{ needs.miri-full.result }}" \ | |
| "- Sanitizers: ${{ needs.sanitizers.result }}" \ | |
| "" \ | |
| "Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}") | |
| existing=$(gh issue list --repo "${{ github.repository }}" --state open \ | |
| --search "$TITLE in:title" --json number --jq '.[0].number // empty') | |
| if [ -n "$existing" ]; then | |
| gh issue comment "$existing" --repo "${{ github.repository }}" --body "$body" | |
| else | |
| gh issue create --repo "${{ github.repository }}" --title "$TITLE" --label bug --body "$body" | |
| fi |