diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6eef6a..abc3d83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,6 +94,9 @@ jobs: with: command: check + - name: Advisory ignore deadlines + run: node scripts/check-ignore-deadlines.mjs + coverage: name: Coverage needs: fmt diff --git a/deny.toml b/deny.toml index 0c4ae88..10c00ee 100644 --- a/deny.toml +++ b/deny.toml @@ -12,8 +12,14 @@ db-urls = ["https://github.com/rustsec/advisory-db"] # Fail only on unmaintained *direct* deps; transitive noise (paste/im-rc/…) stays warn-level notes via ignore if needed. unmaintained = "workspace" ignore = [ - # Transitive via extism → wasmtime ^43; no safe bump until Extism moves. - { id = "RUSTSEC-2026-0222", reason = "wasmtime pin owned by extism 1.30" }, + # Transitive via extism 1.30, which pins wasmtime ^43. RUSTSEC-2026-0222 + # (GHSA-hgjw-h833-99q9) is patched only in >= 46.0.2 and >= 47.0.3; no + # 43.x release fixes it, and extism 1.30.0 is the newest extism release, + # so no bump is possible until extism itself moves. Time-boxed: CI fails + # when the date below passes, forcing a re-evaluation (extism on a + # patched wasmtime, or a local patch). + # expires: 2026-10-31 + { id = "RUSTSEC-2026-0222", reason = "wasmtime 43.x has no patched release; owned by extism 1.30" }, ] [licenses] diff --git a/scripts/check-ignore-deadlines.mjs b/scripts/check-ignore-deadlines.mjs new file mode 100644 index 0000000..276db81 --- /dev/null +++ b/scripts/check-ignore-deadlines.mjs @@ -0,0 +1,51 @@ +#!/usr/bin/env node +/** + * Advisory ignores in deny.toml must be time-boxed. + * + * Every `{ id = "RUSTSEC-…" }` entry in [advisories].ignore needs an + * `# expires: YYYY-MM-DD` line in the comment block directly above it, and + * that date must be in the future. When the date passes, CI fails, forcing a + * re-evaluation of the advisory instead of a silent indefinite exemption. + */ + +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const root = join(fileURLToPath(new URL('.', import.meta.url)), '..') +const target = process.argv[2] ? join(root, process.argv[2]) : join(root, 'deny.toml') + +const lines = readFileSync(target, 'utf8').split(/\r?\n/) + +const pad = (n) => String(n).padStart(2, '0') +const now = new Date() +const today = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}` + +let fails = 0 +for (let i = 0; i < lines.length; i++) { + const entry = lines[i].match(/^\s*\{\s*id\s*=\s*"(RUSTSEC-[^"]+)"/) + if (!entry) continue + const id = entry[1] + + let expires = null + for (let j = i - 1; j >= 0; j--) { + if (!lines[j].trimStart().startsWith('#')) break + const m = lines[j].match(/expires:\s*(\d{4}-\d{2}-\d{2})\b/) + if (m) expires = m[1] + } + + if (!expires) { + console.error( + `deny.toml:${i + 1}: advisory ${id} is ignored without a time-box — add "# expires: YYYY-MM-DD" to its comment block`, + ) + fails++ + } else if (expires < today) { + console.error( + `deny.toml:${i + 1}: ignore for ${id} expired on ${expires} — re-evaluate the advisory and remove or renew the ignore`, + ) + fails++ + } +} + +if (fails > 0) process.exit(1) +console.log('check-ignore-deadlines: clean')