diff --git a/.github/workflows/advisory-inventory.yml b/.github/workflows/advisory-inventory.yml new file mode 100644 index 00000000..f881eac3 --- /dev/null +++ b/.github/workflows/advisory-inventory.yml @@ -0,0 +1,31 @@ +name: Advisory inventory + +# Every open dependency advisory at moderate or above has a triage row in +# SECURITY-EXPOSURE.md (scripts/security/check-exposure-coverage.mjs). It +# runs on every push to main (ci.yml's security-exposure job) and here, once +# a day, because an advisory can be published with nothing merged. A +# failure is one alert about main, answered by one triage row or one bump; +# pull requests are gated only on what they themselves add. + +on: + schedule: + - cron: '41 6 * * *' + workflow_dispatch: + +permissions: + contents: read + +jobs: + inventory: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22 + cache: npm + cache-dependency-path: package-lock.json + - run: npm ci + - name: Every open advisory has a triage row + run: node scripts/security/check-exposure-coverage.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f5a37405..c4b0d005 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -757,6 +757,13 @@ jobs: # SECURITY-EXPOSURE.md. Forces every new alert to be triaged with a # documented decision (override / dismiss / track / patch) instead of # accumulating silently. See also SECURITY-EXPOSURE.md operational notes. + # Dependency advisories, in the place each can be acted on. On a pull + # request: fail only on a vulnerable dependency the pull request's own + # changes bring in (GitHub's dependency review), unless SECURITY-EXPOSURE.md + # already triages it. On main: every open advisory has a triage row (the + # inventory, also run daily by advisory-inventory.yml). Until 0.21.0 the + # inventory ran on every pull request, so each newly published advisory + # turned every open PR red at once, about changes none of them made. security-exposure: runs-on: ubuntu-latest timeout-minutes: 20 @@ -764,13 +771,28 @@ jobs: contents: read steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Advisories already triaged in SECURITY-EXPOSURE.md + if: github.event_name == 'pull_request' + id: triaged + run: echo "ids=$(grep -oE 'GHSA(-[0-9a-z]{4}){3}' SECURITY-EXPOSURE.md | sort -u | paste -sd, -)" >> "$GITHUB_OUTPUT" + - name: No vulnerable dependency added by this pull request + if: github.event_name == 'pull_request' + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + with: + fail-on-severity: moderate + allow-ghsas: ${{ steps.triaged.outputs.ids }} + license-check: false + comment-summary-in-pr: never - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + if: github.event_name != 'pull_request' with: node-version: 22 cache: npm cache-dependency-path: package-lock.json - - run: npm ci - - name: Check exposure coverage + - if: github.event_name != 'pull_request' + run: npm ci + - name: Every open advisory has a triage row + if: github.event_name != 'pull_request' run: node scripts/security/check-exposure-coverage.mjs # Smoke-build the Dockerfile on every PR. The host build (`build` job) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f7bc7817..cc2192b7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -89,7 +89,7 @@ Match the formatting of the file you are editing. CI does not run Prettier, and | `Real clients (ubuntu-latest)` | Claude Code and Gemini CLI connect to this commit through the config `iris-eval install` writes | | `Real clients (macos-latest)` | The same, on macOS | | `Real clients (windows-latest)` | The same, on Windows | - | `security-exposure` | Every open dependency advisory has an assessed row in `SECURITY-EXPOSURE.md` | + | `security-exposure` | On a pull request: no dependency it adds carries an advisory of moderate or above, unless `SECURITY-EXPOSURE.md` already triages it. On main: every open advisory has an assessed row there | | `website-lint-and-typecheck` | Lint, typecheck and production build of `website/` | | `Hardcoded-claim scanner` | No number/claim restated outside the truthbase | | `Truthbase regen vs committed` | `.claims.json` and the rendered files regenerate identical to what you committed | diff --git a/scripts/security/check-exposure-coverage.mjs b/scripts/security/check-exposure-coverage.mjs index 472bd8e0..9d18891b 100644 --- a/scripts/security/check-exposure-coverage.mjs +++ b/scripts/security/check-exposure-coverage.mjs @@ -34,6 +34,14 @@ // not have permission to read Dependabot alerts, so an API-based gate // would require a PAT secret per repo. // +// Where it runs (0.21.0): on every push to main (ci.yml's security-exposure +// job) and daily (advisory-inventory.yml), not on pull requests. There it +// turned every open PR red whenever an advisory was published, about +// changes none of them made; a pull request is now gated by GitHub's +// dependency review on the dependencies it adds, with the GHSA ids triaged +// in SECURITY-EXPOSURE.md allowed. Both remain satisfiable inside the +// change they block. +// // Run locally: node scripts/security/check-exposure-coverage.mjs // Run in CI: same — no secrets needed //