diff --git a/.github/actions/npm-audit-pr-comment/action.yml b/.github/actions/npm-audit-pr-comment/action.yml new file mode 100644 index 00000000..1e2023f0 --- /dev/null +++ b/.github/actions/npm-audit-pr-comment/action.yml @@ -0,0 +1,104 @@ +name: npm audit PR comment +description: > + Posts or updates a PR comment with pre-existing npm audit findings. + Informational only — never fails. Pair with dependency-review-action for gating. + +inputs: + working-directory: + description: Directory containing package.json / package-lock.json + required: true + audit-flags: + description: Extra flags forwarded to npm audit (e.g. --omit=dev) + required: false + default: '' + min-severity: + description: Minimum severity to include in the report (info, low, moderate, high, critical) + required: false + default: high + label: + description: Short label shown in the comment header (e.g. "website", "extension") + required: true + +runs: + using: composite + steps: + - name: Collect audit findings and post PR comment + uses: actions/github-script@v7 + env: + AUDIT_FLAGS: ${{ inputs.audit-flags }} + MIN_SEVERITY: ${{ inputs.min-severity }} + WORK_DIR: ${{ inputs.working-directory }} + LABEL: ${{ inputs.label }} + with: + script: | + const { execSync } = require('child_process') + + let auditOutput + try { + auditOutput = execSync( + `npm audit --json ${process.env.AUDIT_FLAGS}`, + { cwd: process.env.WORK_DIR, stdio: ['pipe', 'pipe', 'pipe'] } + ).toString() + } catch (e) { + auditOutput = e.stdout?.toString() ?? '{}' + } + + const SEVERITY_RANK = ['info', 'low', 'moderate', 'high', 'critical'] + const minSeverity = (process.env.MIN_SEVERITY || 'high').toLowerCase() + const minRank = Math.max(0, SEVERITY_RANK.indexOf(minSeverity)) + + let report + try { + const json = JSON.parse(auditOutput) + const advisories = new Map( + Object.values(json.vulnerabilities ?? {}) + .flatMap(v => (Array.isArray(v.via) ? v.via : [v.via])) + .filter(v => v && typeof v === 'object' && v.url && SEVERITY_RANK.indexOf(v.severity) >= minRank) + .map(v => [v.url, v]) + ) + + const rows = [...advisories.values()].map(a => + `| ${a.severity} | [${a.title}](${a.url}) | \`${a.module_name ?? a.name}\` |` + ) + + report = rows.length === 0 + ? `✅ No vulnerabilities found (severity ≥ ${minSeverity}).` + : [ + `⚠️ **Pre-existing vulnerabilities in \`${process.env.LABEL}\` (severity ≥ ${minSeverity})** — not introduced by this PR, informational only.`, + '', + `| Severity | Advisory | Package |`, + `|----------|----------|---------|`, + ...rows, + '', + `> These were already present on the base branch. Use \`npm audit fix\` or upgrade dependencies to resolve them.`, + ].join('\n') + } catch (_) { + report = `⚠️ Could not parse audit output for \`${process.env.LABEL}\`.` + } + + const marker = `` + const body = `${marker}\n### npm audit — \`${process.env.LABEL}\`\n\n${report}` + + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + per_page: 100, + }) + + const existing = comments.find(c => c.body.includes(marker)) + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }) + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }) + } diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 00000000..9811f37f --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,36 @@ +name: Dependency Review + +# Single repo-wide gate for newly-introduced vulnerabilities. +# dependency-review-action diffs the PR's dependency graph (base -> head), so it +# only flags dependencies CHANGED by this PR — per-package scoping comes for free. +# Pre-existing vulnerabilities are reported separately (non-blocking) by the +# npm-audit-pr-comment action in each package's CI workflow. +on: + pull_request: + branches: [main] + paths: + - '**/package.json' + - '**/package-lock.json' + +jobs: + dependency-review: + name: Dependency Review (fail on new vulnerabilities) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - name: Harden Runner + uses: step-security/harden-runner@v2 + with: + egress-policy: audit + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Dependency Review + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: moderate + vulnerability-check: true + comment-summary-in-pr: on-failure diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 68bacbab..4207312e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -26,6 +26,9 @@ jobs: build: name: Build Documentation runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write steps: - name: Harden Runner uses: step-security/harden-runner@v2 @@ -46,9 +49,13 @@ jobs: working-directory: website run: npm ci - - name: Audit for vulnerabilities - working-directory: website - run: npm audit --audit-level=high + - name: Report pre-existing vulnerabilities on PR + if: github.event_name == 'pull_request' + uses: ./.github/actions/npm-audit-pr-comment + with: + working-directory: ${{ github.workspace }}/website + min-severity: high + label: website - name: Build documentation working-directory: website diff --git a/.github/workflows/lib-collection-scripts-ci.yml b/.github/workflows/lib-collection-scripts-ci.yml index 23b1eff9..71677677 100644 --- a/.github/workflows/lib-collection-scripts-ci.yml +++ b/.github/workflows/lib-collection-scripts-ci.yml @@ -33,11 +33,27 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + security-events: write steps: - name: Checkout code uses: actions/checkout@v4 + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: "fs" + scan-ref: lib + format: "sarif" + output: "trivy-lib-results.sarif" + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v4 + if: always() + with: + sarif_file: "trivy-lib-results.sarif" + category: lib + - name: Setup Node.js uses: actions/setup-node@v4 with: @@ -56,10 +72,6 @@ jobs: working-directory: lib run: npm run lint - - name: Audit for vulnerabilities - working-directory: lib - run: npm audit --audit-level=high - - name: Run tests working-directory: lib run: npm test diff --git a/.github/workflows/vscode-extension-secure-ci.yml b/.github/workflows/vscode-extension-secure-ci.yml index ac1ef64c..6b00e05c 100644 --- a/.github/workflows/vscode-extension-secure-ci.yml +++ b/.github/workflows/vscode-extension-secure-ci.yml @@ -33,7 +33,9 @@ jobs: name: Security Analysis & Vulnerability Scanning runs-on: ubuntu-latest permissions: + contents: read security-events: write + pull-requests: write steps: - name: Harden Runner @@ -62,12 +64,19 @@ jobs: npm ci --fund=false npm run build - - name: Install dependencies with audit + - name: Install dependencies working-directory: ${{ env.EXTENSION_DIR }} shell: bash - run: | - npm ci --audit --fund=false - npm audit --omit=dev --audit-level=moderate + run: npm ci --fund=false + + - name: Report pre-existing vulnerabilities on PR + if: github.event_name == 'pull_request' + uses: ./.github/actions/npm-audit-pr-comment + with: + working-directory: ${{ github.workspace }} + audit-flags: --omit=dev + min-severity: moderate + label: extension - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master