Skip to content

Security Audit

Security Audit #146

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