Security Audit #146
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 Audit | |
| on: | |
| schedule: | |
| # Run daily at midnight UTC | |
| - cron: '0 0 * * *' | |
| workflow_dispatch: | |
| inputs: | |
| ignore_advisories: | |
| description: 'Comma-separated list of advisory IDs to ignore (e.g., RUSTSEC-2024-0384,RUSTSEC-2024-0436)' | |
| required: false | |
| default: 'RUSTSEC-2024-0384,RUSTSEC-2024-0436' | |
| type: string | |
| create_issues: | |
| description: 'Create GitHub issues for new vulnerabilities' | |
| required: false | |
| default: true | |
| type: boolean | |
| workflow_call: | |
| inputs: | |
| ignore_advisories: | |
| description: 'Comma-separated list of advisory IDs to ignore' | |
| required: false | |
| default: 'RUSTSEC-2024-0384,RUSTSEC-2024-0436' | |
| type: string | |
| create_issues: | |
| description: 'Create GitHub issues for new vulnerabilities' | |
| required: false | |
| default: false | |
| type: boolean | |
| outputs: | |
| vulnerabilities_found: | |
| description: 'Number of vulnerabilities found' | |
| value: ${{ jobs.security-audit.outputs.vuln_count }} | |
| env: | |
| CARGO_TERM_COLOR: always | |
| # Concurrency handled by calling workflow (Master Pipeline) | |
| # to prevent deadlocks when used with workflow_call | |
| jobs: | |
| security-audit: | |
| name: Security Audit | |
| runs-on: ubuntu-latest | |
| outputs: | |
| vuln_count: ${{ steps.count_vulns.outputs.count }} | |
| permissions: | |
| contents: read | |
| issues: write | |
| security-events: write | |
| actions: read | |
| checks: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Cache cargo audit database | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.cache/cargo-audit | |
| key: cargo-audit-db-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} | |
| restore-keys: | | |
| cargo-audit-db-${{ runner.os }}- | |
| - name: Run RustSec Security Audit | |
| uses: rustsec/audit-check@v2.0.0 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| # Allow unmaintained dependencies that are indirect through GUI frameworks | |
| # RUSTSEC-2024-0384: instant crate (via iced framework) - unmaintained but actively used | |
| # RUSTSEC-2024-0436: paste crate (via ratatui/iced frameworks) - unmaintained but actively used | |
| ignore: ${{ inputs.ignore_advisories || 'RUSTSEC-2024-0384,RUSTSEC-2024-0436' }} | |
| - name: Install cargo-audit for additional checks | |
| run: | | |
| # Check if cargo-audit is already installed and working | |
| if cargo audit --version 2>/dev/null; then | |
| echo "✅ cargo-audit is already installed: $(cargo audit --version)" | |
| else | |
| echo "📦 Installing cargo-audit..." | |
| if cargo install cargo-audit --locked; then | |
| echo "✅ cargo-audit installed successfully" | |
| else | |
| echo "⚠️ Failed to install cargo-audit with --locked, trying without..." | |
| cargo install cargo-audit || echo "❌ Failed to install cargo-audit" | |
| fi | |
| fi | |
| - name: Run comprehensive audit with JSON output | |
| run: | | |
| # Check if cargo audit supports --format flag | |
| if cargo audit --help 2>&1 | grep -q "format"; then | |
| echo "✅ cargo-audit supports --format flag" | |
| # Try to run cargo audit with JSON output | |
| if cargo audit --format json > audit-results.json 2>&1; then | |
| echo "✅ Audit completed successfully with JSON output" | |
| else | |
| echo "⚠️ JSON format failed, falling back to basic audit..." | |
| # Try basic audit without format flag | |
| if cargo audit > audit-text.txt 2>&1; then | |
| # Parse text output to create JSON | |
| echo '{"vulnerabilities":{"found":false,"count":0,"list":[]}}' > audit-results.json | |
| echo "✅ Basic audit completed, created fallback JSON" | |
| else | |
| # Create empty but valid JSON if audit completely fails | |
| echo '{"vulnerabilities":{"found":false,"count":0,"list":[]}}' > audit-results.json | |
| echo "⚠️ Audit command failed, created empty JSON" | |
| fi | |
| fi | |
| else | |
| echo "⚠️ cargo-audit doesn't support --format flag, using basic audit..." | |
| # Run basic audit without format flag | |
| if cargo audit > audit-text.txt 2>&1; then | |
| # Check if there are any vulnerabilities in the text output | |
| if grep -q "vulnerabilities found" audit-text.txt; then | |
| # Extract count if possible, otherwise default to 1 | |
| vuln_count=$(grep -oP '\d+(?= vulnerabilities found)' audit-text.txt || echo "1") | |
| echo "{\"vulnerabilities\":{\"found\":true,\"count\":$vuln_count,\"list\":[]}}" > audit-results.json | |
| else | |
| echo '{"vulnerabilities":{"found":false,"count":0,"list":[]}}' > audit-results.json | |
| fi | |
| echo "✅ Basic audit completed, created JSON from text output" | |
| else | |
| # Create empty but valid JSON if audit completely fails | |
| echo '{"vulnerabilities":{"found":false,"count":0,"list":[]}}' > audit-results.json | |
| echo "⚠️ Audit command failed, created empty JSON" | |
| fi | |
| fi | |
| # Verify the file exists and is valid JSON | |
| if [ -f "audit-results.json" ]; then | |
| if jq empty audit-results.json 2>/dev/null; then | |
| echo "✅ Valid JSON file created" | |
| else | |
| echo "⚠️ Invalid JSON, creating fallback" | |
| echo '{"vulnerabilities":{"found":false,"count":0,"list":[]}}' > audit-results.json | |
| fi | |
| else | |
| echo "❌ No audit-results.json found, creating fallback" | |
| echo '{"vulnerabilities":{"found":false,"count":0,"list":[]}}' > audit-results.json | |
| fi | |
| - name: Parse audit results and create summary | |
| run: | | |
| cat > audit_summary.sh << 'EOF' | |
| #!/bin/bash | |
| if [ -f "audit-results.json" ] && jq empty audit-results.json 2>/dev/null; then | |
| # Count vulnerabilities by severity (handle empty list gracefully) | |
| high_count=$(jq '[.vulnerabilities.list[]? | select(.advisory.severity == "high")] | length' audit-results.json 2>/dev/null || echo "0") | |
| medium_count=$(jq '[.vulnerabilities.list[]? | select(.advisory.severity == "medium")] | length' audit-results.json 2>/dev/null || echo "0") | |
| low_count=$(jq '[.vulnerabilities.list[]? | select(.advisory.severity == "low")] | length' audit-results.json 2>/dev/null || echo "0") | |
| echo "# Security Audit Summary" > audit_summary.md | |
| echo "" >> audit_summary.md | |
| echo "**Audit Date:** $(date -u '+%Y-%m-%d %H:%M:%S UTC')" >> audit_summary.md | |
| echo "**Repository:** $GITHUB_REPOSITORY" >> audit_summary.md | |
| echo "**Commit:** $GITHUB_SHA" >> audit_summary.md | |
| echo "" >> audit_summary.md | |
| echo "## Vulnerability Counts" >> audit_summary.md | |
| echo "- 🔴 High: $high_count" >> audit_summary.md | |
| echo "- 🟡 Medium: $medium_count" >> audit_summary.md | |
| echo "- 🟢 Low: $low_count" >> audit_summary.md | |
| echo "" >> audit_summary.md | |
| total_vulns=$((high_count + medium_count + low_count)) | |
| if [ $total_vulns -gt 0 ]; then | |
| echo "## Detected Vulnerabilities" >> audit_summary.md | |
| echo "" >> audit_summary.md | |
| jq -r '.vulnerabilities.list[]? | "### " + .advisory.id + " - " + .advisory.title + "\n" + "**Severity:** " + .advisory.severity + "\n" + "**Package:** " + .package.name + " v" + .package.version + "\n" + "**Description:** " + .advisory.description + "\n"' audit-results.json >> audit_summary.md 2>/dev/null || echo "Error parsing vulnerability details" >> audit_summary.md | |
| else | |
| echo "✅ No vulnerabilities detected!" >> audit_summary.md | |
| fi | |
| else | |
| echo "# Security Audit Summary" > audit_summary.md | |
| echo "" >> audit_summary.md | |
| echo "**Audit Date:** $(date -u '+%Y-%m-%d %H:%M:%S UTC')" >> audit_summary.md | |
| echo "**Repository:** $GITHUB_REPOSITORY" >> audit_summary.md | |
| echo "**Commit:** $GITHUB_SHA" >> audit_summary.md | |
| echo "" >> audit_summary.md | |
| if [ -f "audit-results.json" ]; then | |
| echo "⚠️ Audit results file exists but contains invalid JSON." >> audit_summary.md | |
| else | |
| echo "❌ Audit results file not found." >> audit_summary.md | |
| fi | |
| echo "" >> audit_summary.md | |
| echo "The security audit could not be completed. This may be due to:" >> audit_summary.md | |
| echo "- Network connectivity issues" >> audit_summary.md | |
| echo "- cargo-audit installation problems" >> audit_summary.md | |
| echo "- Temporary service unavailability" >> audit_summary.md | |
| fi | |
| cat audit_summary.md | |
| EOF | |
| chmod +x audit_summary.sh | |
| ./audit_summary.sh | |
| - name: Count vulnerabilities | |
| id: count_vulns | |
| run: | | |
| if [ -f "audit-results.json" ] && jq empty audit-results.json 2>/dev/null; then | |
| total=$(jq '[.vulnerabilities.list[]?] | length' audit-results.json 2>/dev/null || echo "0") | |
| echo "✅ Found $total vulnerabilities" | |
| else | |
| total=0 | |
| echo "⚠️ No valid audit results, reporting 0 vulnerabilities" | |
| fi | |
| echo "count=$total" >> $GITHUB_OUTPUT | |
| - name: Upload audit results as artifact | |
| uses: actions/upload-artifact@v6 | |
| if: always() | |
| with: | |
| name: security-audit-results | |
| path: | | |
| audit-results.json | |
| audit_summary.md | |
| retention-days: 30 | |
| - name: Comment audit summary on PR | |
| if: github.event_name == 'pull_request' && github.event.pull_request | |
| continue-on-error: true | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| if (fs.existsSync('audit_summary.md')) { | |
| const summary = fs.readFileSync('audit_summary.md', 'utf8'); | |
| try { | |
| await github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: `## 🛡️ Security Audit Results\n\n${summary}` | |
| }); | |
| } catch (error) { | |
| console.log('Unable to post comment (may be called from workflow_call):', error.message); | |
| } | |
| } | |
| dependency-review: | |
| name: Dependency Review | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Dependency Review | |
| uses: actions/dependency-review-action@v4 | |
| with: | |
| config-file: './.github/dependency-review-config.yml' | |
| comment-summary-in-pr: true | |
| fail-on-severity: high | |
| warn-only: false |