Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions .github/workflows/dependency-vulnerability-scan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
name: Dependency Vulnerability Scan

on:
pull_request:
branches: [main, develop]
paths:
- "package.json"
- "package-lock.json"
- ".github/workflows/dependency-vulnerability-scan.yml"
- "docs/dependency-vulnerability-scanning.md"
- "docs/runbooks/dependency-vulnerability-scanning.md"
push:
branches: [main]
schedule:
# Run before the US business day starts so alerts are available for triage.
- cron: "17 10 * * 1-5"
workflow_dispatch:

permissions:
contents: read
actions: read
security-events: write
pull-requests: write

concurrency:
group: dependency-vulnerability-scan-${{ github.ref }}
cancel-in-progress: true

env:
NODE_VERSION: 20
AUDIT_LEVEL: high

jobs:
dependency-audit:
name: npm audit gate
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm

- name: Install dependencies
run: npm ci

- name: Run npm audit
id: npm-audit
run: |
set +e
npm audit --audit-level=${AUDIT_LEVEL} --json > npm-audit.json
AUDIT_EXIT=$?
node scripts/summarize-npm-audit.mjs npm-audit.json > npm-audit-summary.md
cat npm-audit-summary.md >> "$GITHUB_STEP_SUMMARY"
exit "$AUDIT_EXIT"

- name: Upload npm audit report
if: always()
uses: actions/upload-artifact@v4
with:
name: npm-audit-report
path: |
npm-audit.json
npm-audit-summary.md
retention-days: 30

- name: Comment audit summary on PR
if: github.event_name == 'pull_request' && always()
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: gh pr comment ${{ github.event.pull_request.number }} --body-file npm-audit-summary.md

osv-scanner:
name: OSV lockfile scan
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4

- name: Run OSV Scanner
id: osv-scan
uses: google/osv-scanner-action/osv-scanner-action@v2.2.3
with:
scan-args: |-
--lockfile=package-lock.json
--format=sarif
--output=osv-results.sarif
continue-on-error: true

- name: Upload OSV SARIF to code scanning
if: always() && hashFiles('osv-results.sarif') != ''
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: osv-results.sarif
category: osv-dependencies

- name: Upload OSV artifact
if: always() && hashFiles('osv-results.sarif') != ''
uses: actions/upload-artifact@v4
with:
name: osv-sarif-report
path: osv-results.sarif
retention-days: 30

- name: Fail when OSV finds vulnerabilities
if: steps.osv-scan.outcome == 'failure'
run: exit 1
20 changes: 20 additions & 0 deletions docs/dependency-vulnerability-scanning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Dependency Vulnerability Scanning Architecture

## Scope

The dependency vulnerability scanning pipeline protects this frontend service by checking the npm dependency graph on every dependency-changing pull request, every push to `main`, a weekday scheduled scan, and manual dispatches.

## Architecture

1. **Dependency installation** uses `npm ci` with the repository lockfile so the scan evaluates the same dependency graph used by build and deployment jobs.
2. **npm audit gate** fails the workflow for vulnerabilities at `high` severity or above and publishes a Markdown summary as both a workflow summary and PR comment.
3. **OSV lockfile scan** checks `package-lock.json` against the Open Source Vulnerabilities database and uploads SARIF results to GitHub code scanning for centralized security review.
4. **Artifacts** retain the raw npm audit JSON, Markdown summary, and OSV SARIF for 30 days to support security review and incident follow-up.

## Monitoring and alerting

GitHub Actions workflow failures are the primary alert. The security review queue should monitor the `Dependency Vulnerability Scan` workflow and GitHub code scanning alerts. Pull requests cannot be considered security-ready until this workflow is green or the security team documents a temporary exception.

## Deployment and availability impact

The pipeline is CI-only and does not add application runtime code, network calls, or critical-path latency. It has no direct P99 latency impact and does not change production availability. Dependency updates that pass this gate should continue through the existing release process, including blue-green deployment and canary analysis in the deployment platform.
27 changes: 27 additions & 0 deletions docs/runbooks/dependency-vulnerability-scanning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Dependency Vulnerability Scanning Runbook

## When the scan fails

1. Open the failed `Dependency Vulnerability Scan` workflow run.
2. Review the `npm-audit-report` artifact and the OSV code scanning alert.
3. Prefer safe dependency upgrades with `npm update <package>` or a targeted package version bump.
4. Run the local validation commands below before pushing the fix.
5. If no safe fix is available, request security review and document the accepted risk, affected package, severity, compensating controls, and expiration date in the pull request.

## Local validation

```bash
npm ci
npm audit --audit-level=high
npm run lint
npm test
npm run build
```

## Escalation

Escalate immediately to the security owner when a critical vulnerability affects a direct runtime dependency, has known exploitation, or impacts authentication, signing, transaction handling, or browser-executed code.

## Rollout guidance

Dependency remediation should be deployed through the standard blue-green process. During canary analysis, monitor frontend error rate, Web Vitals, dependency-related console errors, and any security dashboard alerts for the changed package set.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"test:watch": "vitest",
"test:ui": "vitest --ui",
"test:visual": "npx playwright test tests/visual",
"visual:update-baselines": "npx tsx scripts/update-baselines.ts"
"visual:update-baselines": "npx tsx scripts/update-baselines.ts",
"audit:security": "npm audit --audit-level=high"
},
"dependencies": {
"@stellar/stellar-sdk": "^15.1.0",
Expand Down
51 changes: 51 additions & 0 deletions scripts/summarize-npm-audit.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { readFileSync } from 'node:fs';

const [reportPath = 'npm-audit.json'] = process.argv.slice(2);
const report = JSON.parse(readFileSync(reportPath, 'utf8'));
const vulnerabilities = Object.values(report.vulnerabilities ?? {});
const metadata = report.metadata?.vulnerabilities ?? {};

const severityOrder = ['critical', 'high', 'moderate', 'low', 'info'];
const totals = severityOrder
.map((severity) => `${severity}: ${metadata[severity] ?? 0}`)
.join(', ');

const lines = [];
lines.push('## Dependency Vulnerability Scan');
lines.push('');
lines.push(`Audit severity threshold: **${process.env.AUDIT_LEVEL ?? 'high'}**`);
lines.push(`Vulnerability totals: ${totals}`);
lines.push('');

if (vulnerabilities.length === 0) {
lines.push('No npm advisory vulnerabilities were reported.');
process.stdout.write(`${lines.join('\n')}\n`);
process.exit(0);
}

lines.push('| Package | Severity | Direct | Fix available | Via |');
lines.push('|---|---:|:---:|---|---|');

for (const vulnerability of vulnerabilities
.sort((a, b) => severityOrder.indexOf(a.severity) - severityOrder.indexOf(b.severity))
.slice(0, 25)) {
const via = (vulnerability.via ?? [])
.map((entry) => (typeof entry === 'string' ? entry : entry.title ?? entry.source ?? 'advisory'))
.slice(0, 3)
.join('<br>');
const fix = vulnerability.fixAvailable
? typeof vulnerability.fixAvailable === 'object'
? `${vulnerability.fixAvailable.name}@${vulnerability.fixAvailable.version}`
: 'yes'
: 'no';
lines.push(
`| ${vulnerability.name} | ${vulnerability.severity} | ${vulnerability.isDirect ? 'yes' : 'no'} | ${fix} | ${via} |`,
);
}

if (vulnerabilities.length > 25) {
lines.push('');
lines.push(`Showing 25 of ${vulnerabilities.length} vulnerable packages. Download the npm audit artifact for full details.`);
}

process.stdout.write(`${lines.join('\n')}\n`);
Loading