Nightly Security Audit #34
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: Nightly Security Audit | |
| on: | |
| schedule: | |
| # Run every night at 1am UTC | |
| - cron: '0 1 * * *' | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| issues: write | |
| jobs: | |
| full-security-audit: | |
| name: Complete Security Audit | |
| runs-on: ubuntu-latest | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| services: | |
| postgres: | |
| image: postgres:14 | |
| env: | |
| POSTGRES_USER: postgres | |
| POSTGRES_PASSWORD: postgres | |
| POSTGRES_DB: prostaff_test | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| redis: | |
| image: redis:7 | |
| ports: | |
| - 6379:6379 | |
| options: >- | |
| --health-cmd "redis-cli ping" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Set up Ruby | |
| uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd # v1 | |
| with: | |
| ruby-version: 3.4.8 | |
| bundler-cache: true | |
| - name: Setup Database | |
| env: | |
| RAILS_ENV: test | |
| TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/prostaff_test | |
| REDIS_URL: redis://localhost:6379/0 | |
| SECRET_KEY_BASE: nightly_audit_secret_key_base_not_for_production | |
| JWT_SECRET_KEY: nightly_audit_jwt_secret_not_for_production | |
| run: | | |
| bundle exec rails db:create | |
| bundle exec rails db:migrate | |
| - name: Start Rails Server | |
| env: | |
| RAILS_ENV: test | |
| TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/prostaff_test | |
| REDIS_URL: redis://localhost:6379/0 | |
| SECRET_KEY_BASE: nightly_audit_secret_key_base_not_for_production | |
| JWT_SECRET_KEY: nightly_audit_jwt_secret_not_for_production | |
| run: | | |
| bundle exec rails server -p 3333 -e test -d | |
| timeout 60 bash -c 'until curl -sf http://localhost:3333/up; do sleep 2; done' | |
| - name: Install ZAP | |
| run: docker pull zaproxy/zap-stable | |
| - name: Create Reports Directory | |
| run: mkdir -p security_tests/reports/nightly | |
| - name: Run Brakeman | |
| run: | | |
| bundle exec brakeman --rails7 \ | |
| --format json \ | |
| --output security_tests/reports/nightly/brakeman.json \ | |
| --format html \ | |
| --output security_tests/reports/nightly/brakeman.html \ | |
| --no-exit-on-warn || true | |
| - name: Run Bundle Audit | |
| run: | | |
| bundle exec bundler-audit update | |
| bundle exec bundler-audit check \ | |
| --format json \ | |
| --output security_tests/reports/nightly/bundle-audit.json \ | |
| || true | |
| # Also write plain text for human readability | |
| bundle exec bundler-audit check > security_tests/reports/nightly/bundle-audit.txt || true | |
| - name: Run ZAP Baseline Scan | |
| run: | | |
| docker run --rm --network="host" \ | |
| -v "$(pwd)/security_tests/reports/nightly:/zap/wrk:rw" \ | |
| zaproxy/zap-stable \ | |
| zap-baseline.py \ | |
| -t http://localhost:3333 \ | |
| -r zap-baseline.html \ | |
| -J zap-baseline.json || true | |
| - name: Run ZAP API Scan | |
| run: | | |
| docker run --rm --network="host" \ | |
| -v "$(pwd)/security_tests/reports/nightly:/zap/wrk:rw" \ | |
| zaproxy/zap-stable \ | |
| zap-api-scan.py \ | |
| -t http://localhost:3333/api-docs/v1/swagger.yaml \ | |
| -f openapi \ | |
| -r zap-api.html \ | |
| -J zap-api.json || true | |
| - name: Parse Results | |
| id: parse | |
| run: | | |
| # Brakeman | |
| BRAKEMAN_HIGH=$(jq '[.warnings[] | select(.confidence == "High")] | length' \ | |
| security_tests/reports/nightly/brakeman.json 2>/dev/null || echo "0") | |
| BRAKEMAN_TOTAL=$(jq '.warnings | length' \ | |
| security_tests/reports/nightly/brakeman.json 2>/dev/null || echo "0") | |
| # Bundle Audit | |
| if grep -q "Vulnerabilities found" security_tests/reports/nightly/bundle-audit.txt 2>/dev/null; then | |
| VULNERABILITIES="true" | |
| else | |
| VULNERABILITIES="false" | |
| fi | |
| # ZAP | |
| ZAP_HIGH=$(jq '[.site[0].alerts[] | select(.riskcode == "3")] | length' \ | |
| security_tests/reports/nightly/zap-baseline.json 2>/dev/null || echo "0") | |
| ZAP_MEDIUM=$(jq '[.site[0].alerts[] | select(.riskcode == "2")] | length' \ | |
| security_tests/reports/nightly/zap-baseline.json 2>/dev/null || echo "0") | |
| echo "brakeman_high=$BRAKEMAN_HIGH" >> "$GITHUB_OUTPUT" | |
| echo "brakeman_total=$BRAKEMAN_TOTAL" >> "$GITHUB_OUTPUT" | |
| echo "vulnerabilities=$VULNERABILITIES" >> "$GITHUB_OUTPUT" | |
| echo "zap_high=$ZAP_HIGH" >> "$GITHUB_OUTPUT" | |
| echo "zap_medium=$ZAP_MEDIUM" >> "$GITHUB_OUTPUT" | |
| - name: Generate Summary | |
| if: always() | |
| run: | | |
| cat >> "$GITHUB_STEP_SUMMARY" << EOF | |
| # Nightly Security Audit — $(date -u '+%Y-%m-%d %H:%M UTC') | |
| ## Brakeman (SAST) | |
| - Total warnings: ${{ steps.parse.outputs.brakeman_total }} | |
| - High confidence: ${{ steps.parse.outputs.brakeman_high }} | |
| ## Bundle Audit (CVEs) | |
| - Vulnerabilities: ${{ steps.parse.outputs.vulnerabilities }} | |
| ## OWASP ZAP (DAST) | |
| - High risk: ${{ steps.parse.outputs.zap_high }} | |
| - Medium risk: ${{ steps.parse.outputs.zap_medium }} | |
| ## Status | |
| $(if [ "${{ steps.parse.outputs.brakeman_high }}" -gt "0" ] \ | |
| || [ "${{ steps.parse.outputs.vulnerabilities }}" = "true" ] \ | |
| || [ "${{ steps.parse.outputs.zap_high }}" -gt "0" ]; then | |
| echo "⚠️ **ACTION REQUIRED — critical security issues detected!**" | |
| else | |
| echo "✅ No critical issues found." | |
| fi) | |
| EOF | |
| - name: Upload Reports | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: nightly-security-reports-${{ github.run_number }} | |
| path: security_tests/reports/nightly/ | |
| retention-days: 30 | |
| - name: Create GitHub Issue on Failure | |
| if: > | |
| steps.parse.outputs.brakeman_high > 0 || | |
| steps.parse.outputs.vulnerabilities == 'true' || | |
| steps.parse.outputs.zap_high > 0 | |
| uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6 | |
| with: | |
| script: | | |
| const date = new Date().toISOString().split('T')[0]; | |
| const title = `⚠️ Nightly Security Audit Failed — ${date}`; | |
| const body = [ | |
| `## Nightly Security Audit — ${date}`, | |
| '', | |
| `- **Brakeman high**: ${{ steps.parse.outputs.brakeman_high }}`, | |
| `- **CVEs found**: ${{ steps.parse.outputs.vulnerabilities }}`, | |
| `- **ZAP high risk**: ${{ steps.parse.outputs.zap_high }}`, | |
| `- **ZAP medium risk**: ${{ steps.parse.outputs.zap_medium }}`, | |
| '', | |
| `[View run artifacts](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`, | |
| ].join('\n'); | |
| const { data: issues } = await github.rest.issues.listForRepo({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| labels: 'security,automated', | |
| }); | |
| const existing = issues.find(i => i.title.includes('Nightly Security Audit Failed')); | |
| if (existing) { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: existing.number, | |
| body: `## Update — ${new Date().toISOString()}\n\n${body}`, | |
| }); | |
| } else { | |
| await github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title, | |
| body, | |
| labels: ['security', 'automated', 'critical'], | |
| }); | |
| } | |
| - name: Fail on Critical Issues | |
| if: > | |
| steps.parse.outputs.brakeman_high > 0 || | |
| steps.parse.outputs.vulnerabilities == 'true' || | |
| steps.parse.outputs.zap_high > 0 | |
| run: | | |
| echo "::error::Critical security issues detected — check the uploaded reports." | |
| exit 1 |