Skip to content

Nightly Security Audit #34

Nightly Security Audit

Nightly Security Audit #34

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