Skip to content

fix(audit-sprint-4c): modal primitives (Phase 1) + 2 proof migrations #1125

fix(audit-sprint-4c): modal primitives (Phase 1) + 2 proof migrations

fix(audit-sprint-4c): modal primitives (Phase 1) + 2 proof migrations #1125

Workflow file for this run

name: 'Security Scan'
on:
schedule:
# Run security scans daily at 6 AM UTC
- cron: '0 6 * * *'
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
actions: read
security-events: write
env:
NODE_VERSION: '20'
jobs:
dependency_scan:
name: 'Dependency Security Scan'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run npm audit (high severity - prod deps, blocking)
run: npm audit --audit-level=high --omit=dev
- name: Run npm audit (all packages, advisory)
# Advisory only — moderate/low CVEs in dev deps shouldn't block
# the merge gate. The blocking check above (high severity, prod
# deps only) is the actual security floor.
run: npm audit --audit-level=moderate || true
- name: Generate audit report
run: npm audit --json > audit-report.json || true
- name: Upload audit report
uses: actions/upload-artifact@v4
with:
name: security-audit-report
path: audit-report.json
retention-days: 30
code_security:
name: 'Code Security Analysis'
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
actions: read
# Only run CodeQL on push to main or on schedule — skip on PRs to avoid
# "Resource not accessible by integration" SARIF upload failures.
if: github.event_name != 'pull_request'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run CodeQL Analysis
uses: github/codeql-action/init@v3
with:
languages: javascript-typescript
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
security_tests:
name: 'Security Test Verification'
runs-on: ubuntu-latest
# Job-level opt-out so both `npm run build` (prebuild) and
# `npm run start` (prestart) skip the strict env check that would
# otherwise hard-fail on missing Supabase secrets.
env:
CHECK_ENV_SKIP_IN_CI: '1'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Validate required security test environment
env:
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL || vars.NEXT_PUBLIC_SUPABASE_URL || secrets.SUPABASE_URL || vars.SUPABASE_URL }}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY || vars.NEXT_PUBLIC_SUPABASE_ANON_KEY || secrets.SUPABASE_ANON_KEY || vars.SUPABASE_ANON_KEY }}
run: |
missing=0
for key in NEXT_PUBLIC_SUPABASE_URL NEXT_PUBLIC_SUPABASE_ANON_KEY; do
if [ -z "${!key:-}" ]; then
echo "❌ Missing required env var: $key"
missing=1
fi
done
if [ "$missing" -eq 1 ]; then
echo ""
echo "Security tests require Supabase public env vars to boot the app safely."
echo "Set them as GitHub Secrets or Repo Variables (NEXT_PUBLIC_SUPABASE_URL/NEXT_PUBLIC_SUPABASE_ANON_KEY)"
echo "or provide SUPABASE_URL/SUPABASE_ANON_KEY which will be mapped into NEXT_PUBLIC_*."
exit 1
fi
- name: Build application
env:
# Same opt-out pattern as the other CI workflows: skip the
# strict env check in scripts/check-env.js when the workflow
# doesn't carry production Supabase secrets. Vercel builds
# still validate strictly because VERCEL_ENV=production
# short-circuits this opt-out.
CHECK_ENV_SKIP_IN_CI: '1'
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL || vars.NEXT_PUBLIC_SUPABASE_URL || secrets.SUPABASE_URL || vars.SUPABASE_URL }}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY || vars.NEXT_PUBLIC_SUPABASE_ANON_KEY || secrets.SUPABASE_ANON_KEY || vars.SUPABASE_ANON_KEY }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY || vars.SUPABASE_SERVICE_ROLE_KEY || secrets.SUPABASE_SERVICE_KEY || vars.SUPABASE_SERVICE_KEY }}
NEXT_PUBLIC_APP_URL: ${{ secrets.NEXT_PUBLIC_APP_URL || vars.NEXT_PUBLIC_APP_URL }}
NEXT_PUBLIC_SITE_URL: ${{ secrets.NEXT_PUBLIC_SITE_URL || vars.NEXT_PUBLIC_SITE_URL }}
run: npm run build
- name: Run admin security verification tests
run: |
# Start the production server in background
npm run start &
SERVER_PID=$!
# Wait for the server to be ready (up to 60s)
echo "⏳ Waiting for server to start on http://localhost:3000 ..."
for i in $(seq 1 60); do
if curl -s -o /dev/null -w "%{http_code}" http://localhost:3000 | grep -qE "^[1-3]"; then
echo "✅ Server ready after ${i}s"
break
fi
if [ "$i" -eq 60 ]; then
echo "❌ Server failed to start within 60s"
kill $SERVER_PID 2>/dev/null || true
exit 1
fi
sleep 1
done
# Run Playwright tests — config produces list + json + junit reporters
npx playwright test e2e/admin-security-verification.spec.ts \
--config=playwright.config.ts \
--project=chromium \
2>&1 || TEST_EXIT=$?
# Stop the server
kill $SERVER_PID 2>/dev/null || true
exit ${TEST_EXIT:-0}
env:
CI: true
PLAYWRIGHT_BASE_URL: http://localhost:3000
PW_SKIP_WEBSERVER: 1
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL || vars.NEXT_PUBLIC_SUPABASE_URL || secrets.SUPABASE_URL || vars.SUPABASE_URL }}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY || vars.NEXT_PUBLIC_SUPABASE_ANON_KEY || secrets.SUPABASE_ANON_KEY || vars.SUPABASE_ANON_KEY }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY || vars.SUPABASE_SERVICE_ROLE_KEY || secrets.SUPABASE_SERVICE_KEY || vars.SUPABASE_SERVICE_KEY }}
NEXT_PUBLIC_APP_URL: ${{ secrets.NEXT_PUBLIC_APP_URL || vars.NEXT_PUBLIC_APP_URL }}
NEXT_PUBLIC_SITE_URL: ${{ secrets.NEXT_PUBLIC_SITE_URL || vars.NEXT_PUBLIC_SITE_URL }}
- name: Upload security test results
uses: actions/upload-artifact@v4
with:
name: security-test-results
path: |
test-results/
playwright-report/
retention-days: 30
- name: Verify security test results (json report)
if: always()
run: |
if [ ! -f test-results/results.json ]; then
echo "⚠️ JSON report not found — relying on Playwright exit code"
echo "✅ Skipping JSON verification (report not generated)"
exit 0
fi
unexpected=$(jq -r '.stats.unexpected // 0' test-results/results.json)
flaky=$(jq -r '.stats.flaky // 0' test-results/results.json)
skipped=$(jq -r '.stats.skipped // 0' test-results/results.json)
expected=$(jq -r '.stats.expected // 0' test-results/results.json)
echo "Security tests stats: expected=$expected unexpected=$unexpected flaky=$flaky skipped=$skipped"
if [ "$unexpected" != "0" ] && [ "$unexpected" != "null" ]; then
echo "❌ Security tests had unexpected failures"
exit 1
fi
echo "✅ All security tests passed"
secret_scan:
name: 'Secret Detection'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install TruffleHog
run: |
curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh | sh -s -- -b /usr/local/bin
- name: Run TruffleHog secret scan (current files)
shell: bash
run: |
set -euo pipefail
echo "🔍 Scanning current codebase for secrets..."
output=$(mktemp)
excludefile=$(mktemp)
printf 'node_modules\n\.next\n\.git\n\.github\n' > "$excludefile"
if ! trufflehog filesystem . --no-update --fail --only-verified --exclude-paths="$excludefile" --json 2>/dev/null > "$output"; then
sed -n '1,50p' "$output"
echo "❌ TruffleHog found verified secrets in the codebase"
exit 1
fi
echo "✅ TruffleHog: clean ($(wc -l < "$output") findings)"
- name: Run TruffleHog git history scan
shell: bash
run: |
set -euo pipefail
echo "🔍 Scanning git history for secrets..."
output=$(mktemp)
if ! trufflehog git file://. --no-update --fail --only-verified 2>/dev/null > "$output"; then
sed -n '1,20p' "$output"
echo "❌ TruffleHog found verified secrets in git history"
exit 1
fi
sed -n '1,20p' "$output"
- name: Check for hardcoded Supabase service role keys
run: |
echo "🔑 Checking for hardcoded Supabase service role keys..."
if grep -rE "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+" \
--include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" \
--exclude-dir=node_modules --exclude-dir=.next . 2>/dev/null; then
echo "❌ CRITICAL: Hardcoded JWT tokens found in source code!"
echo "These should be environment variables, not hardcoded."
exit 1
else
echo "✅ No hardcoded JWT tokens found"
fi
- name: Check for common secret patterns
run: |
echo "🔐 Checking for common secret patterns..."
FOUND=0
# Check for hardcoded API keys (excluding examples/docs)
if grep -rE "(sk_live_[a-zA-Z0-9]{24}|sk_test_[a-zA-Z0-9]{24})" \
--include="*.ts" --include="*.tsx" --include="*.js" \
--exclude-dir=node_modules --exclude-dir=.next . 2>/dev/null | grep -v "sk_live_\.\.\." | grep -v "sk_test_\.\.\."; then
echo "⚠️ Found potential Stripe keys"
FOUND=1
fi
# Check for AWS keys
if grep -rE "AKIA[A-Z0-9]{16}" \
--include="*.ts" --include="*.tsx" --include="*.js" \
--exclude-dir=node_modules --exclude-dir=.next . 2>/dev/null; then
echo "⚠️ Found potential AWS access keys"
FOUND=1
fi
if [ $FOUND -eq 1 ]; then
echo "❌ Potential secrets found - please review"
exit 1
else
echo "✅ No obvious secret patterns detected"
fi
debug_route_check:
name: 'Debug Route Prevention'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Check for dangerous debug routes
run: |
echo "🚫 Checking for dangerous debug routes in production..."
FOUND=0
# Check for debug-founder route (should not exist)
if [ -d "app/api/debug-founder" ]; then
echo "❌ CRITICAL: app/api/debug-founder exists - must be deleted!"
FOUND=1
fi
# Check for _debug routes that expose sensitive data
if [ -d "app/api/_debug" ]; then
echo "❌ CRITICAL: app/api/_debug exists - must be deleted!"
FOUND=1
fi
# Check for routes that expose environment variables
if grep -rE "process\.env\.(FOUNDER_EMAILS|FOUNDER_USER_IDS|SUPABASE_SERVICE_ROLE)" \
app/api/ --include="*.ts" 2>/dev/null | grep -v "_guard.ts" | grep -v "// " | grep -vE "^\s*//" ; then
echo "⚠️ Found routes that may expose environment variables"
fi
# Verify email test route has auth protection
if [ -f "app/api/email/test/route.ts" ]; then
if ! grep -q "requireFounderAccess" app/api/email/test/route.ts; then
echo "❌ CRITICAL: /api/email/test lacks founder protection!"
FOUND=1
else
echo "✅ /api/email/test is protected"
fi
fi
if [ $FOUND -eq 1 ]; then
echo "❌ Debug route check failed"
exit 1
else
echo "✅ No dangerous debug routes found"
fi
security_summary:
name: 'Security Summary'
runs-on: ubuntu-latest
needs:
[
dependency_scan,
code_security,
security_tests,
secret_scan,
debug_route_check,
]
if: always()
steps:
- name: Security scan summary
run: |
echo "## Security Scan Results" >> $GITHUB_STEP_SUMMARY
echo "| Scan Type | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-----------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Dependency Scan | ${{ needs.dependency_scan.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Code Security | ${{ needs.code_security.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Security Tests | ${{ needs.security_tests.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Secret Detection | ${{ needs.secret_scan.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Debug Route Check | ${{ needs.debug_route_check.result }} |" >> $GITHUB_STEP_SUMMARY
- name: Critical security check
if: |
needs.code_security.result == 'failure' ||
needs.security_tests.result == 'failure' ||
needs.secret_scan.result == 'failure' ||
needs.debug_route_check.result == 'failure'
run: |
echo "❌ CRITICAL: Security checks failed - deployment blocked"
echo ""
echo " code_security: ${{ needs.code_security.result }}"
echo " security_tests: ${{ needs.security_tests.result }}"
echo " secret_scan: ${{ needs.secret_scan.result }}"
echo " debug_routes: ${{ needs.debug_route_check.result }}"
exit 1