fix(audit-sprint-4c): modal primitives (Phase 1) + 2 proof migrations #1125
Workflow file for this run
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 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 |