name: Security
on:
push:
branches: [ "main", "develop" ]
pull_request:
branches: [ "main", "develop" ]
schedule:
- cron: '0 3 * * *'
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
permissions:
contents: read
security-events: write
jobs:
audit:
name: Security Audit
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ runner.os }}-cargo-audit-${{ hashFiles('**/Cargo.lock') }}
- name: Install cargo-audit
run: cargo install cargo-audit
- name: Run cargo audit
run: |
cargo audit --json > audit-report.json || AUDIT_EXIT_CODE=$?
# Display results
echo "đ Security Audit Results:"
cargo audit || true
# Check for vulnerabilities
if [ -s audit-report.json ]; then
VULN_COUNT=$(jq '.vulnerabilities.count // 0' audit-report.json)
if [ "$VULN_COUNT" -gt 0 ]; then
echo "â Found $VULN_COUNT vulnerabilities"
exit 1
else
echo "â
No vulnerabilities found"
fi
else
echo "â ī¸ Audit report is empty or malformed"
fi
- name: Upload audit report
uses: actions/upload-artifact@v4
with:
name: security-audit-report
path: audit-report.json
if: always()
clippy-security:
name: Clippy Security Lints
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target
key: ${{ runner.os }}-cargo-clippy-security-${{ hashFiles('**/Cargo.lock') }}
- name: Run security-focused clippy lints
run: |
cargo clippy --all-targets --all-features -- \
-W clippy::suspicious \
-W clippy::security \
-W clippy::panic \
-W clippy::unwrap_used \
-W clippy::expect_used \
-W clippy::indexing_slicing \
-W clippy::integer_arithmetic \
-W clippy::float_arithmetic \
-W clippy::unsafe_derive_deserialize \
-W clippy::mem_forget \
-W clippy::todo \
-W clippy::unimplemented \
-W clippy::unreachable \
-W clippy::as_conversions \
-W clippy::cast_possible_truncation \
-W clippy::cast_possible_wrap \
-W clippy::cast_precision_loss \
-W clippy::cast_sign_loss \
-W clippy::integer_division \
-W clippy::lossy_float_literal \
-W clippy::string_slice
dependency-review:
name: Dependency Review
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Dependency Review
uses: actions/dependency-review-action@v4
with:
fail-on-severity: moderate
comment-summary-in-pr: on-failure
supply-chain-security:
name: Supply Chain Security
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ runner.os }}-cargo-supply-chain-${{ hashFiles('**/Cargo.lock') }}
- name: Install cargo-deny
run: cargo install cargo-deny
- name: Create deny.toml if it doesn't exist
run: |
if [ ! -f deny.toml ]; then
cat > deny.toml << 'EOF'
[graph]
targets = [
{ triple = "x86_64-unknown-linux-gnu" },
{ triple = "x86_64-unknown-linux-musl" },
{ triple = "x86_64-pc-windows-msvc" },
{ triple = "x86_64-apple-darwin" },
]
[licenses]
allow = [
"MIT",
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"BSD-2-Clause",
"BSD-3-Clause",
"ISC",
"Unicode-DFS-2016",
"CC0-1.0",
]
confidence-threshold = 0.8
exceptions = []
[[licenses.clarify]]
name = "ring"
version = "*"
expression = "MIT AND ISC AND OpenSSL"
license-files = [
{ path = "LICENSE", hash = 0xbd0eed23 }
]
[bans]
multiple-versions = "warn"
wildcards = "allow"
highlight = "all"
# Deny crates with known security issues
deny = [
{ name = "openssl", version = "<0.10.55" },
{ name = "chrono", version = "<0.4.20" },
]
skip = [
{ name = "windows-sys" },
{ name = "winapi" },
]
skip-tree = [
{ name = "criterion" },
]
[advisories]
version = 2
db-path = "~/.cargo/advisory-db"
db-urls = ["https://github.com/rustsec/advisory-db"]
unmaintained = "all"
yanked = "deny"
ignore = []
[sources]
unknown-registry = "warn"
unknown-git = "warn"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
allow-git = []
EOF
fi
- name: Check licenses
run: cargo deny check licenses
- name: Check advisories
run: cargo deny check advisories
- name: Check bans
run: cargo deny check bans
- name: Check sources
run: cargo deny check sources
secrets-scan:
name: Secrets Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run TruffleHog OSS
uses: trufflesecurity/trufflehog@main
with:
path: ./
extra_args: --debug --only-verified
code-ql:
name: CodeQL Analysis
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event_name == 'schedule'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: rust
queries: security-extended,security-and-quality
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target
key: ${{ runner.os }}-cargo-codeql-${{ hashFiles('**/Cargo.lock') }}
- name: Build for CodeQL
run: cargo build --all-features
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:rust"
unsafe-code-review:
name: Unsafe Code Review
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Check for unsafe code
run: |
echo "đ Scanning for unsafe code blocks..."
UNSAFE_COUNT=$(find src -name "*.rs" -exec grep -l "unsafe" {} \; | wc -l)
if [ $UNSAFE_COUNT -gt 0 ]; then
echo "â ī¸ Found unsafe code in $UNSAFE_COUNT files:"
find src -name "*.rs" -exec grep -l "unsafe" {} \; | while read file; do
echo " - $file"
grep -n "unsafe" "$file" | head -5
done
# Don't fail the build for unsafe code, just report it
echo ""
echo "âšī¸ Unsafe code detected. Please ensure it's properly reviewed and documented."
else
echo "â
No unsafe code found"
fi
security-report:
name: Security Report
runs-on: ubuntu-latest
needs: [audit, clippy-security, supply-chain-security, unsafe-code-review]
if: always()
steps:
- name: Download audit report
uses: actions/download-artifact@v4
with:
name: security-audit-report
path: ./reports
continue-on-error: true
- name: Generate security summary
run: |
echo "# đ Security Analysis Report" > security-summary.md
echo "" >> security-summary.md
echo "## Job Results" >> security-summary.md
echo "- **Audit**: ${{ needs.audit.result }}" >> security-summary.md
echo "- **Clippy Security**: ${{ needs.clippy-security.result }}" >> security-summary.md
echo "- **Supply Chain**: ${{ needs.supply-chain-security.result }}" >> security-summary.md
echo "- **Unsafe Code Review**: ${{ needs.unsafe-code-review.result }}" >> security-summary.md
echo "" >> security-summary.md
if [ -f "./reports/audit-report.json" ]; then
echo "## Vulnerability Summary" >> security-summary.md
VULN_COUNT=$(jq '.vulnerabilities.count // 0' ./reports/audit-report.json)
echo "- **Total Vulnerabilities**: $VULN_COUNT" >> security-summary.md
fi
echo "" >> security-summary.md
echo "Report generated on: $(date)" >> security-summary.md
- name: Upload security report
uses: actions/upload-artifact@v4
with:
name: security-report
path: security-summary.md
- name: Check overall security status
run: |
if [ "${{ needs.audit.result }}" = "failure" ]; then
echo "â Security audit failed - vulnerabilities detected"
exit 1
elif [ "${{ needs.clippy-security.result }}" = "failure" ]; then
echo "â Security lints failed"
exit 1
elif [ "${{ needs.supply-chain-security.result }}" = "failure" ]; then
echo "â Supply chain security checks failed"
exit 1
else
echo "â
All security checks passed"
fi