Repository navigation
Security Compliance Scanning #40
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 Compliance Scanning | |
| on: | |
| pull_request: | |
| branches: [main, staging] | |
| push: | |
| branches: [main] | |
| schedule: | |
| # Daily scan at 02:00 UTC to catch new advisories published between code changes | |
| - cron: '0 2 * * *' | |
| env: | |
| CARGO_TERM_COLOR: always | |
| RUST_BACKTRACE: 1 | |
| jobs: | |
| # --------------------------------------------------------------------------- | |
| # 1. Dependency vulnerability scanning (cargo audit) | |
| # --------------------------------------------------------------------------- | |
| dependency-scan: | |
| name: Dependency Vulnerability Scan | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache cargo registry | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} | |
| - name: Install cargo-audit | |
| run: cargo install cargo-audit --locked | |
| - name: Run cargo audit | |
| id: cargo_audit | |
| run: | | |
| cargo audit --json > audit-report.json 2>&1 || true | |
| # Exit non-zero if any critical or high severity vulnerabilities found | |
| # that are not in the allowlist | |
| cargo audit --deny warnings --deny unsound 2>&1 | |
| continue-on-error: true | |
| - name: Check for critical/high vulnerabilities (fail CI) | |
| run: | | |
| if [ -f audit-report.json ]; then | |
| CRITICAL=$(cat audit-report.json | python3 -c " | |
| import json, sys | |
| data = json.load(sys.stdin) | |
| vulns = data.get('vulnerabilities', {}).get('list', []) | |
| critical_high = [v for v in vulns if v.get('advisory', {}).get('cvss', {}).get('score', 0) >= 7.0] | |
| print(len(critical_high)) | |
| " 2>/dev/null || echo "0") | |
| if [ "$CRITICAL" -gt "0" ]; then | |
| echo "::error::Found $CRITICAL critical/high severity dependency vulnerabilities. CI build failed." | |
| exit 1 | |
| fi | |
| fi | |
| - name: Upload dependency scan report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: dependency-scan-report-${{ github.sha }} | |
| path: audit-report.json | |
| retention-days: 90 | |
| - name: Alert on new critical advisory (scheduled scan) | |
| if: github.event_name == 'schedule' && failure() | |
| run: | | |
| echo "::error::Daily dependency scan detected new critical vulnerability advisory. Security team must be notified immediately." | |
| # In production: POST to security alerting webhook | |
| # curl -X POST "$SECURITY_ALERT_WEBHOOK" \ | |
| # -H "Content-Type: application/json" \ | |
| # -d '{"event":"new_critical_advisory","scan":"cargo_audit","sha":"${{ github.sha }}"}' | |
| # --------------------------------------------------------------------------- | |
| # 2. Static Application Security Testing (SAST) | |
| # --------------------------------------------------------------------------- | |
| sast: | |
| name: Static Application Security Testing | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install Rust toolchain with clippy | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: clippy | |
| - name: Cache cargo registry | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} | |
| - name: Run security-focused clippy lints | |
| run: | | |
| cargo clippy \ | |
| --all-targets \ | |
| --all-features \ | |
| -- \ | |
| -D warnings \ | |
| -D clippy::all \ | |
| -W clippy::pedantic \ | |
| -D clippy::unwrap_used \ | |
| -D clippy::expect_used \ | |
| -D clippy::panic \ | |
| -D clippy::integer_arithmetic \ | |
| -D clippy::as_conversions \ | |
| -D clippy::indexing_slicing \ | |
| -W clippy::suspicious \ | |
| -W clippy::correctness \ | |
| 2>&1 | tee sast-report.txt | |
| - name: Scan for unsafe Rust usage | |
| run: | | |
| # Detect unsafe blocks without justification comments | |
| UNSAFE_COUNT=$(grep -rn "unsafe {" src/ --include="*.rs" | grep -v "// SAFETY:" | wc -l || echo "0") | |
| if [ "$UNSAFE_COUNT" -gt "0" ]; then | |
| echo "::warning::Found $UNSAFE_COUNT unsafe blocks without SAFETY justification comments" | |
| grep -rn "unsafe {" src/ --include="*.rs" | grep -v "// SAFETY:" | |
| fi | |
| - name: Scan for hardcoded secrets patterns | |
| run: | | |
| # Basic pattern scan — gitleaks handles the full scan below | |
| PATTERNS=( | |
| "password\s*=\s*['\"][^'\"]{8,}" | |
| "secret\s*=\s*['\"][^'\"]{8,}" | |
| "api_key\s*=\s*['\"][^'\"]{8,}" | |
| "private_key\s*=\s*['\"][^'\"]{8,}" | |
| ) | |
| FOUND=0 | |
| for pattern in "${PATTERNS[@]}"; do | |
| if grep -rniE "$pattern" src/ --include="*.rs" 2>/dev/null; then | |
| FOUND=1 | |
| fi | |
| done | |
| if [ "$FOUND" -eq "1" ]; then | |
| echo "::error::Potential hardcoded secrets detected in source code" | |
| exit 1 | |
| fi | |
| - name: Upload SAST report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: sast-report-${{ github.sha }} | |
| path: sast-report.txt | |
| retention-days: 90 | |
| # --------------------------------------------------------------------------- | |
| # 3. Secrets detection (gitleaks) | |
| # --------------------------------------------------------------------------- | |
| secrets-detection: | |
| name: Secrets Detection | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # Full history for first-time git history scan | |
| - name: Run gitleaks on full git history | |
| uses: gitleaks/gitleaks-action@v2 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }} | |
| with: | |
| args: detect --source . --log-opts="--all" --report-format json --report-path gitleaks-report.json | |
| - name: Upload secrets scan report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: secrets-scan-report-${{ github.sha }} | |
| path: gitleaks-report.json | |
| retention-days: 90 | |
| - name: Alert on detected secrets | |
| if: failure() | |
| run: | | |
| echo "::error::Secrets detected in repository. Rotate all exposed credentials immediately and purge git history." | |
| # --------------------------------------------------------------------------- | |
| # 4. Container image vulnerability scanning (Trivy) | |
| # --------------------------------------------------------------------------- | |
| container-scan: | |
| name: Container Image Vulnerability Scan | |
| runs-on: ubuntu-latest | |
| if: github.ref == 'refs/heads/main' || github.event_name == 'schedule' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Build Docker image | |
| run: docker build -t aframp-backend:${{ github.sha }} . | |
| - name: Run Trivy vulnerability scanner | |
| uses: aquasecurity/trivy-action@master | |
| with: | |
| image-ref: aframp-backend:${{ github.sha }} | |
| format: json | |
| output: trivy-report.json | |
| severity: CRITICAL,HIGH,MEDIUM,LOW | |
| exit-code: 0 # We handle exit code manually below | |
| - name: Fail on critical/high CVEs | |
| run: | | |
| CRITICAL=$(cat trivy-report.json | python3 -c " | |
| import json, sys | |
| data = json.load(sys.stdin) | |
| count = 0 | |
| for result in data.get('Results', []): | |
| for vuln in result.get('Vulnerabilities', []): | |
| if vuln.get('Severity') in ('CRITICAL', 'HIGH'): | |
| count += 1 | |
| print(count) | |
| " 2>/dev/null || echo "0") | |
| if [ "$CRITICAL" -gt "0" ]; then | |
| echo "::error::Found $CRITICAL critical/high CVEs in container image. Image push blocked." | |
| exit 1 | |
| fi | |
| - name: Upload container scan report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: container-scan-report-${{ github.sha }} | |
| path: trivy-report.json | |
| retention-days: 90 | |
| - name: Push image to registry (only if scan passed) | |
| if: success() && github.ref == 'refs/heads/main' | |
| run: | | |
| echo "Container scan passed — image is safe to push to registry" | |
| # docker tag aframp-backend:${{ github.sha }} ${{ secrets.REGISTRY }}/aframp-backend:${{ github.sha }} | |
| # docker push ${{ secrets.REGISTRY }}/aframp-backend:${{ github.sha }} | |
| # --------------------------------------------------------------------------- | |
| # 5. Weekly production image rescan | |
| # --------------------------------------------------------------------------- | |
| production-image-rescan: | |
| name: Weekly Production Image Rescan | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'schedule' | |
| steps: | |
| - name: Pull current production image | |
| run: | | |
| echo "Pulling production image for rescan..." | |
| # docker pull ${{ secrets.REGISTRY }}/aframp-backend:production | |
| - name: Rescan production image against updated CVE database | |
| run: | | |
| echo "Rescanning production image with latest CVE database..." | |
| # trivy image --severity CRITICAL,HIGH --format json \ | |
| # --output production-rescan-report.json \ | |
| # ${{ secrets.REGISTRY }}/aframp-backend:production | |
| - name: Alert on new critical CVEs in production image | |
| if: failure() | |
| run: | | |
| echo "::error::New critical CVE detected in currently deployed production image. Immediate patching required." | |
| # --------------------------------------------------------------------------- | |
| # 6. Ingest findings into vulnerability management system | |
| # --------------------------------------------------------------------------- | |
| ingest-findings: | |
| name: Ingest Scan Findings | |
| runs-on: ubuntu-latest | |
| needs: [dependency-scan, sast, secrets-detection] | |
| if: always() && github.ref == 'refs/heads/main' | |
| steps: | |
| - name: Download all scan reports | |
| uses: actions/download-artifact@v4 | |
| - name: Ingest findings via API | |
| run: | | |
| echo "Ingesting scan findings into vulnerability management system..." | |
| # In production, parse each report and POST to: | |
| # POST /api/admin/security/findings/ingest | |
| # with appropriate severity, source, and raw_finding payload |