CVE Monitor #2
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: CVE Monitor | |
| # Nightly supply-chain CVE monitor — closes Phase 0 audit finding C-14. | |
| # Plan: docs/plan/bfsi-v1/04-commits.md commit C-032 (owner Role 22). | |
| # | |
| # Runs `npm audit --json` (always available) plus `npx osv-scanner -r .` | |
| # (Google's OSV scanner — covers ecosystems npm audit doesn't, eg cargo, | |
| # gradle, swift packages used by the mobile sub-projects). | |
| # | |
| # If either scanner reports any vulnerability with severity `high` or | |
| # `critical`, the workflow opens a GitHub issue with the scanner output | |
| # attached and sends an email alert to the address held in the | |
| # SECURITY_ALERT_EMAIL secret. Both signals are intentional belt-and- | |
| # braces — Slack alerts are wired in by C-129..C-142 (alert tuning). | |
| on: | |
| schedule: | |
| # Nightly at 00:00 UTC (05:30 IST — well before the 09:30 IST standup). | |
| - cron: '0 0 * * *' | |
| workflow_dispatch: | |
| concurrency: | |
| group: cve-monitor | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read | |
| issues: write | |
| jobs: | |
| scan: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| cache-dependency-path: | | |
| package-lock.json | |
| dashboard/package-lock.json | |
| website/package-lock.json | |
| - name: Install root dependencies | |
| run: npm ci | |
| - name: Install dashboard dependencies | |
| run: npm --prefix dashboard ci | |
| - name: Install website dependencies | |
| run: npm --prefix website ci | |
| - name: Run CVE scanners | |
| id: scan | |
| # The helper script exits 0 when no high/critical CVEs are found | |
| # and exits non-zero (1) when at least one is found. We capture | |
| # the exit status into a step output so the follow-up "open | |
| # issue" + "email alert" steps can fire conditionally without | |
| # short-circuiting the whole job. | |
| run: | | |
| set +e | |
| ./scripts/cve-monitor.sh > /tmp/cve-monitor.log 2>&1 | |
| status=$? | |
| echo "status=${status}" >> "$GITHUB_OUTPUT" | |
| cat /tmp/cve-monitor.log | |
| # Always succeed at this step so artefact upload + alerting | |
| # run; the job itself fails at the final guard step below. | |
| exit 0 | |
| - name: Upload scanner output | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: cve-monitor-log | |
| path: /tmp/cve-monitor.log | |
| retention-days: 30 | |
| if-no-files-found: warn | |
| - name: Open GitHub issue on high/critical finding | |
| if: steps.scan.outputs.status != '0' | |
| uses: actions/github-script@v7 | |
| env: | |
| SCAN_LOG_PATH: /tmp/cve-monitor.log | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const path = process.env.SCAN_LOG_PATH; | |
| let log = '(scanner log missing)'; | |
| try { | |
| log = fs.readFileSync(path, 'utf8'); | |
| } catch (err) { | |
| core.warning(`Could not read scanner log at ${path}: ${err.message}`); | |
| } | |
| // Cap at 60 KB so the issue body stays under GitHub's limit. | |
| if (log.length > 60_000) { | |
| log = log.slice(0, 60_000) + '\n\n…truncated…'; | |
| } | |
| const today = new Date().toISOString().slice(0, 10); | |
| const title = `CVE monitor: high/critical finding ${today}`; | |
| const body = [ | |
| '## Nightly CVE monitor — high/critical finding', | |
| '', | |
| `Run: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, | |
| `Workflow: \`${context.workflow}\``, | |
| `Commit: ${context.sha}`, | |
| '', | |
| 'Closes audit-findings.md C-14 instrumentation; see', | |
| '`docs/plan/bfsi-v1/04-commits.md` commit C-032 for context.', | |
| '', | |
| '### Scanner output', | |
| '', | |
| '```', | |
| log, | |
| '```', | |
| ].join('\n'); | |
| await github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title, | |
| body, | |
| labels: ['security', 'cve-monitor'], | |
| }); | |
| - name: Send email alert on high/critical finding | |
| if: steps.scan.outputs.status != '0' | |
| env: | |
| SECURITY_ALERT_EMAIL: ${{ secrets.SECURITY_ALERT_EMAIL }} | |
| run: | | |
| if [ -z "${SECURITY_ALERT_EMAIL:-}" ]; then | |
| echo "::warning::SECURITY_ALERT_EMAIL secret is not set; skipping email alert." | |
| exit 0 | |
| fi | |
| # The repo's mail relay is exercised by tests/email.test.ts; the | |
| # workflow itself uses a thin sendmail wrapper rather than | |
| # introducing a new GitHub Action dep (DP6: every dep is an ADR). | |
| subject="[ZeroAuth] CVE monitor: high/critical finding $(date -u +%F)" | |
| { | |
| echo "To: ${SECURITY_ALERT_EMAIL}" | |
| echo "From: cve-monitor@zeroauth.dev" | |
| echo "Subject: ${subject}" | |
| echo "" | |
| echo "The nightly CVE monitor (.github/workflows/cve-monitor.yml)" | |
| echo "found at least one vulnerability rated high or critical." | |
| echo "" | |
| echo "Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" | |
| echo "" | |
| echo "Scanner output:" | |
| echo "----" | |
| cat /tmp/cve-monitor.log || echo "(log unavailable)" | |
| } > /tmp/cve-monitor.mail | |
| if command -v sendmail >/dev/null 2>&1; then | |
| sendmail -t < /tmp/cve-monitor.mail | |
| echo "Email queued via sendmail to ${SECURITY_ALERT_EMAIL}." | |
| else | |
| echo "::warning::sendmail not available on runner; falling back to printing the message." | |
| cat /tmp/cve-monitor.mail | |
| fi | |
| - name: Fail job if any high/critical CVE was found | |
| if: steps.scan.outputs.status != '0' | |
| run: | | |
| echo "::error::High or critical CVE detected; see uploaded log and opened issue." | |
| exit 1 |