feat(cve/scanner): add CVE research pipeline extension #75

Merged
stack72 merged 5 commits from cve-scanner into main 2026-06-25 19:27:31 +00:00
Owner

Summary

  • Adds @swamp/cve/scanner — a daily CVE research pipeline that queries NVD, GitHub Advisory DB, and CISA KEV for HIGH/CRITICAL vulnerabilities
  • Classifies CVEs by real-world impact (supply-chain, infrastructure, open-source library, vendor product, web app) and scores each for actionability
  • Surfaces Extension Candidates (score 7+) that warrant a dedicated swamp scanner, like dirtyfrag or mini-shai-hulud
  • Bundles a workflow that runs the scan and posts a rich Discord embed summary via @keeb/discord
  • Designed for daily cron via GitHub Actions with markdown report in job summary

What's included

File Purpose
cve_research.ts Model with research method — queries 3 CVE sources, deduplicates, classifies, scores
discord_webhook_env.ts Extension method for @keeb/discord/webhook that reads DISCORD_WEBHOOK_URL from env
cve_research_report.ts Markdown report with classification, actionability scoring, and scoring methodology
cve-research-to-discord.yaml Bundled workflow: scan → Discord notification
*_test.ts 32 unit tests across all source files

Dependencies

  • @keeb/discord (declared in manifest, auto-pulled)

Test plan

  • deno task test — 32/32 passing
  • swamp extension push manifest.yaml --dry-run — passes with no blocking warnings
  • swamp extension fmt manifest.yaml — clean
  • Adversarial review completed (15 dimensions)
  • CI pipeline validates on merge
## Summary - Adds `@swamp/cve/scanner` — a daily CVE research pipeline that queries NVD, GitHub Advisory DB, and CISA KEV for HIGH/CRITICAL vulnerabilities - Classifies CVEs by real-world impact (supply-chain, infrastructure, open-source library, vendor product, web app) and scores each for actionability - Surfaces Extension Candidates (score 7+) that warrant a dedicated swamp scanner, like dirtyfrag or mini-shai-hulud - Bundles a workflow that runs the scan and posts a rich Discord embed summary via `@keeb/discord` - Designed for daily cron via GitHub Actions with markdown report in job summary ## What's included | File | Purpose | |---|---| | `cve_research.ts` | Model with `research` method — queries 3 CVE sources, deduplicates, classifies, scores | | `discord_webhook_env.ts` | Extension method for `@keeb/discord/webhook` that reads `DISCORD_WEBHOOK_URL` from env | | `cve_research_report.ts` | Markdown report with classification, actionability scoring, and scoring methodology | | `cve-research-to-discord.yaml` | Bundled workflow: scan → Discord notification | | `*_test.ts` | 32 unit tests across all source files | ## Dependencies - `@keeb/discord` (declared in manifest, auto-pulled) ## Test plan - [x] `deno task test` — 32/32 passing - [x] `swamp extension push manifest.yaml --dry-run` — passes with no blocking warnings - [x] `swamp extension fmt manifest.yaml` — clean - [x] Adversarial review completed (15 dimensions) - [ ] CI pipeline validates on merge
feat(cve/scanner): add CVE research pipeline extension
Some checks failed
CI / aws models - sample check (pull_request) Has been cancelled
CI / cve/dirtyfrag - fmt (pull_request) Has been cancelled
CI / cve/dirtyfrag - lint (pull_request) Has been cancelled
CI / cve/dirtyfrag - test (pull_request) Has been cancelled
CI / cve/mini-shai-hulud - check (pull_request) Has been cancelled
CI / cve/mini-shai-hulud - lint (pull_request) Has been cancelled
CI / cve/mini-shai-hulud - test (pull_request) Has been cancelled
CI / cve/scanner - check (pull_request) Has been cancelled
CI / cve/scanner - fmt (pull_request) Has been cancelled
CI / cve/scanner - test (pull_request) Has been cancelled
CI / cve/dirtyfrag - lockfile up to date (pull_request) Has been cancelled
CI / cve/mini-shai-hulud - lockfile up to date (pull_request) Has been cancelled
CI / cve/scanner - lockfile up to date (pull_request) Has been cancelled
CI / software-factory - check (pull_request) Has been cancelled
CI / software-factory - lint (pull_request) Has been cancelled
CI / software-factory - test (pull_request) Has been cancelled
CI / model/digitalocean - check (pull_request) Has been cancelled
CI / datastore/s3 - test (pull_request) Has been cancelled
CI / datastore/s3 - lockfile up to date (pull_request) Has been cancelled
CI / datastore/gcs - lint (pull_request) Has been cancelled
CI / issue-lifecycle - lint (pull_request) Has been cancelled
CI / kubernetes - check (pull_request) Has been cancelled
CI / workflows/gcs-bootstrap - check (pull_request) Has been cancelled
CI / workflows/s3-bootstrap - fmt (pull_request) Has been cancelled
CI / workflows/s3-bootstrap - lint (pull_request) Has been cancelled
CI / cve/dirtyfrag - check (pull_request) Has been cancelled
CI / cve/mini-shai-hulud - fmt (pull_request) Has been cancelled
CI / cve/scanner - lint (pull_request) Has been cancelled
CI / software-factory - fmt (pull_request) Has been cancelled
CI / Actions Audit (pull_request) Successful in 50s
88bdd4f200
Daily CVE research pipeline that queries NVD, GitHub Advisory DB, and
CISA KEV for HIGH/CRITICAL vulnerabilities, classifies them by real-world
impact (supply-chain, infrastructure, open-source library, vendor product),
and scores each for actionability to identify which CVEs warrant a
dedicated swamp extension.

Includes:
- @swamp/cve/scanner model with `research` method
- @swamp/cve/scanner-report with classification and scoring
- Discord webhook extension (sendFromEnv) reading URL from env var
- Bundled workflow for scan + Discord notification
- 32 unit tests across all three source files
- Dependency on @keeb/discord for notifications

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat(cve/researcher): add CVE research pipeline extension
Some checks failed
CI / cve/dirtyfrag - fmt (pull_request) Has been cancelled
CI / cve/dirtyfrag - test (pull_request) Has been cancelled
CI / cve/mini-shai-hulud - check (pull_request) Has been cancelled
CI / cve/mini-shai-hulud - fmt (pull_request) Has been cancelled
CI / cve/mini-shai-hulud - lint (pull_request) Has been cancelled
CI / cve/scanner - check (pull_request) Has been cancelled
CI / cve/scanner - fmt (pull_request) Has been cancelled
CI / cve/scanner - lint (pull_request) Has been cancelled
CI / cve/scanner - test (pull_request) Has been cancelled
CI / software-factory - lint (pull_request) Has been cancelled
CI / software-factory - test (pull_request) Has been cancelled
CI / software-factory - lockfile up to date (pull_request) Has been cancelled
CI / model/digitalocean - check (pull_request) Has been cancelled
CI / model/hetzner-cloud - check (pull_request) Has been cancelled
CI / cve/mini-shai-hulud - lockfile up to date (pull_request) Has been cancelled
CI / kubernetes - lint (pull_request) Has been cancelled
CI / cve/dirtyfrag - check (pull_request) Has been cancelled
CI / cve/dirtyfrag - lint (pull_request) Has been cancelled
CI / software-factory - fmt (pull_request) Has been cancelled
CI / software-factory - check (pull_request) Has been cancelled
CI / codegen - check (pull_request) Has been cancelled
CI / vault/azure-kv - test (pull_request) Has been cancelled
CI / datastore/gcs - lint (pull_request) Has been cancelled
CI / issue-lifecycle - lint (pull_request) Has been cancelled
CI / vault/1password - lint (pull_request) Has been skipped
CI / datastore/s3 - test (pull_request) Has been cancelled
CI / ssh - lint (pull_request) Has been cancelled
CI / kubernetes - test (pull_request) Has been cancelled
CI / workflows/gcs-bootstrap - test (pull_request) Has been cancelled
CI / workflows/s3-bootstrap - check (pull_request) Has been cancelled
662fe3b02d
Rename from cve/scanner to cve/researcher — this extension researches
what CVEs are new in the world, it doesn't scan systems for known
vulnerabilities (that's what dirtyfrag and mini-shai-hulud do).

Daily CVE research pipeline that queries NVD, GitHub Advisory DB, and
CISA KEV for HIGH/CRITICAL vulnerabilities, classifies them by real-world
impact (supply-chain, infrastructure, open-source library, vendor product),
and scores each for actionability to identify which CVEs warrant a
dedicated swamp extension.

Includes:
- @swamp/cve/researcher model with `research` method
- @swamp/cve/researcher-report with classification and scoring
- Discord webhook extension (sendFromEnv) reading URL from env var
- Bundled workflow for scan + Discord notification
- 32 unit tests across all three source files
- Dependency on @keeb/discord for notifications

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix ci
Some checks failed
CI / cve/mini-shai-hulud - fmt (pull_request) Successful in 1m13s
CI / cve/mini-shai-hulud - lint (pull_request) Successful in 1m14s
CI / cve/researcher - lint (pull_request) Failing after 1m14s
CI / cve/researcher - fmt (pull_request) Successful in 1m14s
CI / cve/researcher - check (pull_request) Failing after 1m14s
CI / cve/mini-shai-hulud - test (pull_request) Successful in 1m16s
CI / cve/dirtyfrag - test (pull_request) Successful in 1m32s
CI / software-factory - check (pull_request) Has been skipped
CI / software-factory - lint (pull_request) Has been skipped
CI / software-factory - fmt (pull_request) Has been skipped
CI / software-factory - lockfile up to date (pull_request) Has been skipped
CI / software-factory - test (pull_request) Has been skipped
CI / model/digitalocean - check (pull_request) Has been skipped
CI / model/hetzner-cloud - check (pull_request) Has been skipped
CI / model/digitalocean - lockfile up to date (pull_request) Has been skipped
CI / gcp models - lockfiles up to date (pull_request) Has been skipped
CI / cve/researcher - test (pull_request) Failing after 53s
CI / cloudflare models - sample check (pull_request) Has been skipped
CI / codegen - fmt (pull_request) Has been skipped
CI / codegen - check (pull_request) Has been skipped
CI / cloudflare models - lockfiles up to date (pull_request) Has been skipped
CI / cve/dirtyfrag - lockfile up to date (pull_request) Successful in 54s
CI / codegen - lint (pull_request) Has been skipped
CI / cve/researcher - lockfile up to date (pull_request) Successful in 55s
CI / Dependency Audit (pull_request) Successful in 5m16s
CI / cve/mini-shai-hulud - lockfile up to date (pull_request) Successful in 56s
CI / Claude Code Review (pull_request) Has been skipped
CI / Adversarial Code Review (pull_request) Has been skipped
CI / CI Security Review (pull_request) Successful in 1m44s
CI / Merge Gate (pull_request) Failing after 27s
1a1782e5f2
Author
Owner

CI Security Review

Summary

The diff adds researcher to the cve matrix in two existing jobs (cve-check at line 364 and cve-lockfile at line 394). This extends CI coverage to the new cve/researcher extension directory using the same check/lint/fmt/test and lockfile-verification pattern already applied to dirtyfrag and mini-shai-hulud.

Analysis

1. Prompt Injection — No LLM-invoking jobs were modified. Not applicable.

2. Expression Injection${{ matrix.cve }} is used in working-directory and job name fields. The matrix values are hardcoded strings defined in the workflow file (dirtyfrag, mini-shai-hulud, researcher) — not attacker-controlled. No injection risk.

3. Dangerous Triggers — Unchanged. The workflow triggers on pull_request (not pull_request_target), which runs in the PR head context without access to base-repo secrets. Safe.

4. Supply Chain — The affected jobs use actions/checkout@v6 and denoland/setup-deno@v2, both from trusted publishers. No new actions introduced.

5. Permissions — The affected jobs inherit the workflow-level permissions: contents: read, which is the minimum needed for checkout + deno check/test. No escalation.

6. Secret Exposure — No secrets are referenced in cve-check or cve-lockfile. Not applicable.

7. Auto-merge & Trust Boundaries — Not applicable to these jobs.

Critical / High

None.

Medium

None.

Low

None.

Verdict

PASS — Security-neutral change. Adds a new matrix entry (researcher) to two existing CI jobs using an established, safe pattern. No new triggers, permissions, secrets, actions, or LLM interactions introduced.

## CI Security Review ### Summary The diff adds `researcher` to the `cve` matrix in two existing jobs (`cve-check` at line 364 and `cve-lockfile` at line 394). This extends CI coverage to the new `cve/researcher` extension directory using the same check/lint/fmt/test and lockfile-verification pattern already applied to `dirtyfrag` and `mini-shai-hulud`. ### Analysis **1. Prompt Injection** — No LLM-invoking jobs were modified. Not applicable. **2. Expression Injection** — `${{ matrix.cve }}` is used in `working-directory` and job `name` fields. The matrix values are hardcoded strings defined in the workflow file (`dirtyfrag`, `mini-shai-hulud`, `researcher`) — not attacker-controlled. No injection risk. **3. Dangerous Triggers** — Unchanged. The workflow triggers on `pull_request` (not `pull_request_target`), which runs in the PR head context without access to base-repo secrets. Safe. **4. Supply Chain** — The affected jobs use `actions/checkout@v6` and `denoland/setup-deno@v2`, both from trusted publishers. No new actions introduced. **5. Permissions** — The affected jobs inherit the workflow-level `permissions: contents: read`, which is the minimum needed for checkout + deno check/test. No escalation. **6. Secret Exposure** — No secrets are referenced in `cve-check` or `cve-lockfile`. Not applicable. **7. Auto-merge & Trust Boundaries** — Not applicable to these jobs. ### Critical / High None. ### Medium None. ### Low None. ### Verdict **PASS** — Security-neutral change. Adds a new matrix entry (`researcher`) to two existing CI jobs using an established, safe pattern. No new triggers, permissions, secrets, actions, or LLM interactions introduced.
fix(cve/researcher): add TypeScript strict mode annotations for CI
Some checks failed
CI / cve/dirtyfrag - test (pull_request) Successful in 1m19s
CI / cve/dirtyfrag - check (pull_request) Successful in 1m20s
CI / cve/mini-shai-hulud - test (pull_request) Successful in 1m20s
CI / cve/dirtyfrag - fmt (pull_request) Successful in 1m21s
CI / cve/mini-shai-hulud - check (pull_request) Successful in 1m20s
CI / cve/researcher - fmt (pull_request) Successful in 1m10s
CI / cve/researcher - check (pull_request) Successful in 1m21s
CI / Dependency Audit (pull_request) Successful in 4m24s
CI / software-factory - check (pull_request) Has been skipped
CI / software-factory - fmt (pull_request) Has been skipped
CI / model/digitalocean - check (pull_request) Has been skipped
CI / software-factory - lint (pull_request) Has been skipped
CI / software-factory - test (pull_request) Has been skipped
CI / software-factory - lockfile up to date (pull_request) Has been skipped
CI / gcp models - lockfiles up to date (pull_request) Has been skipped
CI / cloudflare models - sample check (pull_request) Has been skipped
CI / codegen - lockfile up to date (pull_request) Has been skipped
CI / cloudflare models - lockfiles up to date (pull_request) Has been skipped
CI / codegen - fmt (pull_request) Has been skipped
CI / codegen - check (pull_request) Has been skipped
CI / codegen - lint (pull_request) Has been skipped
CI / cve/researcher - lint (pull_request) Successful in 50s
CI / cve/researcher - test (pull_request) Successful in 55s
CI / cve/dirtyfrag - lockfile up to date (pull_request) Successful in 55s
CI / cve/mini-shai-hulud - lockfile up to date (pull_request) Successful in 58s
CI / cve/researcher - lockfile up to date (pull_request) Successful in 58s
CI / CI Security Review (pull_request) Successful in 2m13s
CI / Claude Code Review (pull_request) Failing after 3m19s
CI / Adversarial Code Review (pull_request) Failing after 4m11s
CI / Merge Gate (pull_request) Failing after 24s
22fa4e0034
Add proper type annotations to all function parameters, callbacks,
and variables across model, report, and test files to pass deno check
in strict mode. Fix lint issues (require-await) in test mocks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

CI Security Review

Summary

The diff adds researcher to the cve matrix in two jobs (cve-check at line 364 and cve-lockfile at line 394). This is a security-neutral change — it extends existing hardcoded matrix values to include an additional CVE extension directory for CI checks.

Critical / High

No critical or high severity findings.

Medium

No medium severity findings.

Low

No low severity findings.

Pre-existing observations (not blocking)

The overall workflow has a well-considered security posture:

  • Trigger: Uses pull_request (not pull_request_target), preventing secrets from being exposed to fork PR code.
  • Permissions: Workflow-level contents: read with job-level escalation to pull-requests: write only for the review-posting jobs.
  • Supply chain: Claude binary download is pinned to a specific version and verified with SHA-256 checksum. actions/checkout@v6 and denoland/setup-deno@v2 use tag pins which is acceptable for trusted publishers.
  • LLM tool scoping: Claude review agents have tightly scoped --allowedTools (read-only git commands, Read, Grep, and specific tee/touch paths). Write and Edit are explicitly disallowed.
  • Shell safety: Event fields (base.sha, head.sha) are passed via env: blocks rather than direct ${{ }} interpolation in run: blocks, preventing expression injection.

Verdict

PASS — Adding researcher to the CVE matrix is a minimal, security-neutral change that follows the same safe patterns used by the other matrix entries.

## CI Security Review ### Summary The diff adds `researcher` to the `cve` matrix in two jobs (`cve-check` at line 364 and `cve-lockfile` at line 394). This is a security-neutral change — it extends existing hardcoded matrix values to include an additional CVE extension directory for CI checks. ### Critical / High No critical or high severity findings. ### Medium No medium severity findings. ### Low No low severity findings. ### Pre-existing observations (not blocking) The overall workflow has a well-considered security posture: - **Trigger**: Uses `pull_request` (not `pull_request_target`), preventing secrets from being exposed to fork PR code. - **Permissions**: Workflow-level `contents: read` with job-level escalation to `pull-requests: write` only for the review-posting jobs. - **Supply chain**: Claude binary download is pinned to a specific version and verified with SHA-256 checksum. `actions/checkout@v6` and `denoland/setup-deno@v2` use tag pins which is acceptable for trusted publishers. - **LLM tool scoping**: Claude review agents have tightly scoped `--allowedTools` (read-only git commands, `Read`, `Grep`, and specific `tee`/`touch` paths). `Write` and `Edit` are explicitly disallowed. - **Shell safety**: Event fields (`base.sha`, `head.sha`) are passed via `env:` blocks rather than direct `${{ }}` interpolation in `run:` blocks, preventing expression injection. ### Verdict **PASS** — Adding `researcher` to the CVE matrix is a minimal, security-neutral change that follows the same safe patterns used by the other matrix entries.
Author
Owner

Code Review

Blocking Issues

  1. npm:zod@4 is not an exact version pincve_research.ts:1 and discord_webhook_env.ts:1 both import from npm:zod@4. This is a major-version specifier (matches any 4.x.y), not an exact pin. CLAUDE.md requires "Pin all npm dependencies to exact versions (no ranges)", and the local cve/researcher/CLAUDE.md reinforces this with an example of the correct form (npm:lodash-es@4.17.21). Should be e.g. npm:zod@4.3.0 (whatever the current 4.x release is).

Suggestions

  1. fetchJson retry backoff only covers 429/503cve_research.ts:185-205: for non-429/503 HTTP errors (e.g. 500 from NVD or GitHub), the loop calls continue immediately with no sleep, so three rapid-fire requests are made with no delay. Consider adding backoff for all retryable errors, or at minimum not retrying 4xx errors that will never succeed (400, 401, 403).

  2. Discord embed field value may exceed the 1024-char Discord limitcve_research.ts:605-619: each field value is built by joining up to 5 CVE summaries (up to 300 chars each) with \n\n, yielding up to ~1508 chars in a single field. Discord rejects webhook POSTs with field values over 1024 chars. Consider capping the combined field value length.

  3. || vs ?? for webhook URL fallbackdiscord_webhook_env.ts:30: context.globalArgs.webhookUrl || Deno.env.get(...) means an empty-string webhookUrl silently falls through to the env var rather than being caught at the "no webhook URL" guard. ?? is more precise here since an empty string is also invalid and should hit the same error path.

  4. No integration tests for the execute pathsresearch.execute (network calls to NVD, GitHub, CISA KEV) and the success path of sendFromEnv.execute (Discord POST) are untested. The existing tests cover schema validation and report rendering well, but the execution paths that do actual I/O have no coverage. Local mock servers (Deno.serve({ port: 0 })) would let these be tested without hitting live APIs.

## Code Review ### Blocking Issues 1. **`npm:zod@4` is not an exact version pin** — `cve_research.ts:1` and `discord_webhook_env.ts:1` both import from `npm:zod@4`. This is a major-version specifier (matches any 4.x.y), not an exact pin. CLAUDE.md requires "Pin all npm dependencies to exact versions (no ranges)", and the local `cve/researcher/CLAUDE.md` reinforces this with an example of the correct form (`npm:lodash-es@4.17.21`). Should be e.g. `npm:zod@4.3.0` (whatever the current 4.x release is). ### Suggestions 1. **`fetchJson` retry backoff only covers 429/503** — `cve_research.ts:185-205`: for non-429/503 HTTP errors (e.g. 500 from NVD or GitHub), the loop calls `continue` immediately with no sleep, so three rapid-fire requests are made with no delay. Consider adding backoff for all retryable errors, or at minimum not retrying 4xx errors that will never succeed (400, 401, 403). 2. **Discord embed field value may exceed the 1024-char Discord limit** — `cve_research.ts:605-619`: each field value is built by joining up to 5 CVE summaries (up to 300 chars each) with `\n\n`, yielding up to ~1508 chars in a single field. Discord rejects webhook POSTs with field values over 1024 chars. Consider capping the combined field value length. 3. **`||` vs `??` for webhook URL fallback** — `discord_webhook_env.ts:30`: `context.globalArgs.webhookUrl || Deno.env.get(...)` means an empty-string `webhookUrl` silently falls through to the env var rather than being caught at the "no webhook URL" guard. `??` is more precise here since an empty string is also invalid and should hit the same error path. 4. **No integration tests for the execute paths** — `research.execute` (network calls to NVD, GitHub, CISA KEV) and the success path of `sendFromEnv.execute` (Discord POST) are untested. The existing tests cover schema validation and report rendering well, but the execution paths that do actual I/O have no coverage. Local mock servers (`Deno.serve({ port: 0 })`) would let these be tested without hitting live APIs.
Author
Owner

Adversarial Review

Critical / High

  1. Discord embed field values exceed 1024-char limitcve/researcher/extensions/models/cve_research.ts:604-619

    formatDiscordSummary joins up to 5 CVE entries into a single embed field value. Each entry includes a title (~60 chars) and a cveSummary (up to 300 chars), so the worst-case field value is ~1850 characters. Discord rejects embed field values over 1024 characters with HTTP 400.

    Breaking input: 4+ extension-candidate CVEs with descriptions longer than 200 characters — a realistic scenario during a busy disclosure day. The Discord API returns 400, sendFromEnv throws, the workflow step fails, and no notification is sent for exactly the highest-priority results.

    Suggested fix: Cap each field value at 1024 characters. Either reduce cveSummary to ~150 chars when there are 3+ items, or split across multiple embed fields (Discord allows up to 25 fields per embed). The same issue applies to the monitors field at lines 630-645.

Medium

  1. Non-atomic state file writecve/researcher/extensions/models/cve_research.ts:156-168

    saveState uses Deno.writeTextFile, which is not atomic. If the process is killed mid-write (OOM, CI timeout, SIGKILL), the state file is left partially written. Next run, loadState catches the JSON parse error and falls back to { lastCheck: null, seenIds: new Set() }, causing every CVE in the window to be re-flagged as "new" and producing a duplicate Discord blast.

    Suggested fix: Write to a temp file, then Deno.rename it over the target (rename is atomic on POSIX).

  2. fetchJson retries non-transient HTTP errors with zero delaycve/researcher/extensions/models/cve_research.ts:192-195

    For HTTP errors other than 429/503 (e.g. 400, 401, 403, 404), the code sets lastError and continues with no backoff. This fires 3 requests in rapid succession against APIs that have already returned a definitive error, wasting budget and risking IP-level rate limiting from NVD or GitHub.

    Suggested fix: Either don't retry 4xx errors at all (they're not transient), or at minimum apply the same exponential backoff as the 429/503 path.

  3. Unbounded seenIds set growthcve/researcher/extensions/models/cve_research.ts:815-819

    state.seenIds is append-only — IDs are added every run but never pruned. With 3 sources × 40 results/source × daily runs, the set grows by ~120 IDs/day after dedup. After a year the state file holds 40k+ entries. Not a crash, but the file grows monotonically and loadState/saveState get progressively slower.

    Suggested fix: Prune IDs older than 2× the max lookback window (180 days) on each save, or cap the set size.

  4. Discord webhook fetch has no timeoutcve/researcher/extensions/models/discord_webhook_env.ts:47

    The fetch call to Discord has no AbortSignal.timeout(). Compare with cve_research.ts:182 which correctly uses AbortSignal.timeout(30_000). If Discord is unresponsive, the workflow step hangs indefinitely until the CI runner's global timeout kills it.

    Suggested fix: Add signal: AbortSignal.timeout(30_000) to the fetch options.

Low

  1. Response body not consumed on retry pathscve/researcher/extensions/models/cve_research.ts:185-195

    When fetchJson gets a 429, 503, or other non-OK response, the response body is never read before the retry loop continues. In Deno, an unconsumed response body keeps the underlying TCP connection alive. Over 3 retries × 3 sources × 2 severities, this could temporarily hold ~18 connections open.

    Suggested fix: Add await resp.body?.cancel() before continue on retry paths.

  2. "dos" category keyword is overly broadcve/researcher/extensions/models/cve_research.ts:242

    desc.includes("dos") matches substrings like "todos" or "dosage" in lowercased descriptions, causing false-positive categorization as "dos". Unlikely in CVE descriptions but possible.

    Suggested fix: Use a word-boundary regex like /\bdos\b/ or match only the full phrase "denial of service".

Verdict

FAIL — The Discord embed field-value overflow (finding #1) will cause the notification pipeline to error on the days it matters most (many high-severity CVEs). The missing fetch timeout (finding #5) can hang the workflow indefinitely. Fix these two before merging.

## Adversarial Review ### Critical / High 1. **Discord embed field values exceed 1024-char limit** — `cve/researcher/extensions/models/cve_research.ts:604-619` `formatDiscordSummary` joins up to 5 CVE entries into a single embed field value. Each entry includes a title (~60 chars) and a `cveSummary` (up to 300 chars), so the worst-case field value is ~1850 characters. Discord rejects embed field values over 1024 characters with HTTP 400. **Breaking input:** 4+ extension-candidate CVEs with descriptions longer than 200 characters — a realistic scenario during a busy disclosure day. The Discord API returns 400, `sendFromEnv` throws, the workflow step fails, and no notification is sent for exactly the highest-priority results. **Suggested fix:** Cap each field value at 1024 characters. Either reduce `cveSummary` to ~150 chars when there are 3+ items, or split across multiple embed fields (Discord allows up to 25 fields per embed). The same issue applies to the monitors field at lines 630-645. ### Medium 2. **Non-atomic state file write** — `cve/researcher/extensions/models/cve_research.ts:156-168` `saveState` uses `Deno.writeTextFile`, which is not atomic. If the process is killed mid-write (OOM, CI timeout, SIGKILL), the state file is left partially written. Next run, `loadState` catches the JSON parse error and falls back to `{ lastCheck: null, seenIds: new Set() }`, causing every CVE in the window to be re-flagged as "new" and producing a duplicate Discord blast. **Suggested fix:** Write to a temp file, then `Deno.rename` it over the target (rename is atomic on POSIX). 3. **`fetchJson` retries non-transient HTTP errors with zero delay** — `cve/researcher/extensions/models/cve_research.ts:192-195` For HTTP errors other than 429/503 (e.g. 400, 401, 403, 404), the code sets `lastError` and `continue`s with no backoff. This fires 3 requests in rapid succession against APIs that have already returned a definitive error, wasting budget and risking IP-level rate limiting from NVD or GitHub. **Suggested fix:** Either don't retry 4xx errors at all (they're not transient), or at minimum apply the same exponential backoff as the 429/503 path. 4. **Unbounded `seenIds` set growth** — `cve/researcher/extensions/models/cve_research.ts:815-819` `state.seenIds` is append-only — IDs are added every run but never pruned. With 3 sources × 40 results/source × daily runs, the set grows by ~120 IDs/day after dedup. After a year the state file holds 40k+ entries. Not a crash, but the file grows monotonically and `loadState`/`saveState` get progressively slower. **Suggested fix:** Prune IDs older than 2× the max lookback window (180 days) on each save, or cap the set size. 5. **Discord webhook fetch has no timeout** — `cve/researcher/extensions/models/discord_webhook_env.ts:47` The `fetch` call to Discord has no `AbortSignal.timeout()`. Compare with `cve_research.ts:182` which correctly uses `AbortSignal.timeout(30_000)`. If Discord is unresponsive, the workflow step hangs indefinitely until the CI runner's global timeout kills it. **Suggested fix:** Add `signal: AbortSignal.timeout(30_000)` to the fetch options. ### Low 6. **Response body not consumed on retry paths** — `cve/researcher/extensions/models/cve_research.ts:185-195` When `fetchJson` gets a 429, 503, or other non-OK response, the response body is never read before the retry loop continues. In Deno, an unconsumed response body keeps the underlying TCP connection alive. Over 3 retries × 3 sources × 2 severities, this could temporarily hold ~18 connections open. **Suggested fix:** Add `await resp.body?.cancel()` before `continue` on retry paths. 7. **`"dos"` category keyword is overly broad** — `cve/researcher/extensions/models/cve_research.ts:242` `desc.includes("dos")` matches substrings like "todos" or "dosage" in lowercased descriptions, causing false-positive categorization as "dos". Unlikely in CVE descriptions but possible. **Suggested fix:** Use a word-boundary regex like `/\bdos\b/` or match only the full phrase "denial of service". ### Verdict **FAIL** — The Discord embed field-value overflow (finding #1) will cause the notification pipeline to error on the days it matters most (many high-severity CVEs). The missing fetch timeout (finding #5) can hang the workflow indefinitely. Fix these two before merging.
fix(cve/researcher): pin zod version and cap Discord embed field values
All checks were successful
CI / cve/mini-shai-hulud - test (pull_request) Successful in 1m16s
CI / cve/researcher - check (pull_request) Successful in 1m19s
CI / software-factory - check (pull_request) Has been skipped
CI / software-factory - fmt (pull_request) Has been skipped
CI / software-factory - lint (pull_request) Has been skipped
CI / software-factory - test (pull_request) Has been skipped
CI / software-factory - lockfile up to date (pull_request) Has been skipped
CI / model/digitalocean - check (pull_request) Has been skipped
CI / model/hetzner-cloud - check (pull_request) Has been skipped
CI / model/digitalocean - lockfile up to date (pull_request) Has been skipped
CI / aws models - sample check (pull_request) Has been skipped
CI / aws models - lockfiles up to date (pull_request) Has been skipped
CI / cve/researcher - lint (pull_request) Successful in 1m0s
CI / cve/researcher - test (pull_request) Successful in 1m0s
CI / model/hetzner-cloud - lockfile up to date (pull_request) Has been skipped
CI / gcp models - sample check (pull_request) Has been skipped
CI / cve/dirtyfrag - lockfile up to date (pull_request) Successful in 54s
CI / cloudflare models - sample check (pull_request) Has been skipped
CI / gcp models - lockfiles up to date (pull_request) Has been skipped
CI / cloudflare models - lockfiles up to date (pull_request) Has been skipped
CI / codegen - check (pull_request) Has been skipped
CI / codegen - lint (pull_request) Has been skipped
CI / codegen - fmt (pull_request) Has been skipped
CI / cve/mini-shai-hulud - lockfile up to date (pull_request) Successful in 55s
CI / cve/researcher - lockfile up to date (pull_request) Successful in 58s
CI / codegen - lockfile up to date (pull_request) Has been skipped
CI / CI Security Review (pull_request) Successful in 1m22s
CI / Adversarial Code Review (pull_request) Successful in 4m38s
CI / Claude Code Review (pull_request) Successful in 6m41s
CI / Merge Gate (pull_request) Successful in 27s
259e2a3e62
- Pin npm:zod@4 to npm:zod@4.3.6 in both model files per repo CLAUDE.md
  requirement for exact version pins
- Cap Discord embed field values at 1024 chars (Discord API limit) by
  building field content incrementally and stopping before overflow.
  Previously 4+ CVEs with long descriptions would produce ~1850 char
  field values causing HTTP 400 from Discord.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

CI Security Review

Summary

The change adds researcher to the cve matrix in two jobs (cve-check at line 364 and cve-lockfile at line 394). This is a purely additive change that extends existing CI check/lint/fmt/test and lockfile verification to cover the new cve/researcher extension directory.

The added matrix value is a static, hardcoded string — not derived from any user-controlled input. It flows into:

  • Job names (cosmetic): cve/researcher - check, etc.
  • working-directory: cve/researcher (safe — static path)

No new triggers, permissions, secrets, actions, or LLM integrations are introduced by this change.

Critical / High

None.

Medium

None.

Low

None.

Verdict

PASS — Security-neutral change. Adding a static matrix entry to run existing CI jobs against a new extension directory introduces no new attack surface.

## CI Security Review ### Summary The change adds `researcher` to the `cve` matrix in two jobs (`cve-check` at line 364 and `cve-lockfile` at line 394). This is a purely additive change that extends existing CI check/lint/fmt/test and lockfile verification to cover the new `cve/researcher` extension directory. The added matrix value is a **static, hardcoded string** — not derived from any user-controlled input. It flows into: - Job names (cosmetic): `cve/researcher - check`, etc. - `working-directory: cve/researcher` (safe — static path) No new triggers, permissions, secrets, actions, or LLM integrations are introduced by this change. ### Critical / High None. ### Medium None. ### Low None. ### Verdict **PASS** — Security-neutral change. Adding a static matrix entry to run existing CI jobs against a new extension directory introduces no new attack surface.
Author
Owner

Adversarial Review

Medium

  1. Deduplication priority is backwards from the commentcve/researcher/extensions/models/cve_research.ts:782-804

    The comment on line 782 says "preferring CISA KEV > NVD > GitHub", but the iteration order is [...ghResult.findings, ...nvdResult.findings, ...kevResult.findings]. The first occurrence wins for all base fields (description, severity, severityScore, source, published). Only knownExploited and categories are merged from later duplicates.

    Breaking example: A CVE appears in both GitHub Advisory and CISA KEV. The record is stored with source: "GITHUB", GitHub's description, and GitHub's severity score. The knownExploited flag is correctly merged from KEV, but the source field will show "GitHub Advisory" in reports and Discord embeds instead of "CISA KEV". Users see a misleadingly attributed record for actively exploited vulnerabilities.

    Suggested fix: Either reverse the iteration order to [...kevResult.findings, ...nvdResult.findings, ...ghResult.findings] (so KEV data is stored first and kept), or update the merge logic to overwrite base fields when a higher-priority source is encountered. Or, if the current behavior is intentional, fix the comment.

  2. Discord webhook fetch has no timeoutcve/researcher/extensions/models/discord_webhook_env.ts:47-51

    The fetch(webhookUrl, ...) call has no signal: AbortSignal.timeout(...), unlike fetchJson in cve_research.ts which uses a 30-second timeout. If the Discord webhook endpoint hangs (DNS resolution stall, connection accepted but no response), this call blocks indefinitely.

    Breaking example: Discord experiences a partial outage where connections are accepted but responses stall. The workflow step hangs forever, preventing the workflow from completing or retrying.

    Suggested fix: Add signal: AbortSignal.timeout(15_000) to the fetch options, matching the pattern used in fetchJson.

  3. categorizeCve false-positive on "dos" substringcve/researcher/extensions/models/cve_research.ts:241

    desc.includes("dos") matches any description containing the substring "dos" — not just "denial of service" or "DoS". While desc is lowercased so proper nouns won't match, product names or technical terms containing "dos" (e.g., "msdos", "freedos", "dosfstools", "kudos") would incorrectly categorize the CVE as "dos".

    Breaking example: A CVE description mentioning "FreeDOS" or "dosfstools" gets tagged dos when it's actually a memory corruption bug, skewing category-based filtering and Discord embed grouping.

    Suggested fix: Use word-boundary matching: /\bdos\b/ or /\bdenial of service\b|\bdos\b/ instead of desc.includes("dos"). The "denial of service" long form is already checked separately, so the short-form check could be tightened to desc.includes(" dos ") or a regex with word boundaries.

Low

  1. State file write is non-atomiccve/researcher/extensions/models/cve_research.ts:160-168

    Deno.writeTextFile is not atomic. If the process crashes mid-write, cve-research-state.json could be corrupted. On next run, loadState catches the parse error and returns empty state, causing all CVEs to be re-flagged as "new". Unlikely in practice since the file is small and writes are fast, but a write-then-rename pattern would eliminate the risk.

  2. seenIds set grows without boundcve/researcher/extensions/models/cve_research.ts:833-835

    Every run adds new CVE IDs to seenIds but old IDs are never pruned. Over months of daily runs, this set grows without limit. At ~50 new IDs/day, after a year that's ~18,000 entries (~500KB JSON). Not operationally dangerous but worth noting — a periodic prune (e.g., drop IDs older than 180 days) would keep the file bounded.

  3. Non-retryable HTTP errors retried without delaycve/researcher/extensions/models/cve_research.ts:193-195

    In fetchJson, HTTP errors other than 429/503 (e.g., 400, 401, 403, 404) hit continue without any delay, immediately retrying. A 404 will be retried 3 times in rapid succession with no hope of success.

  4. Discord webhook has no retry mechanismcve/researcher/extensions/models/discord_webhook_env.ts:47-58

    Unlike the CVE source queries which retry on transient errors, the Discord webhook POST has no retry logic. A transient 429 or 503 from Discord will immediately fail the workflow step.

Verdict

PASS — No critical or high severity issues found. The deduplication priority mismatch (Medium #1) produces incorrect source attribution but doesn't affect the functional correctness of scoring or alerting since knownExploited is correctly merged. The missing timeout (Medium #2) is a robustness gap but not a correctness bug. The "dos" substring match (Medium #3) is a minor classification accuracy issue. All three mediums are worth fixing but none block the merge.

## Adversarial Review ### Medium 1. **Deduplication priority is backwards from the comment** — `cve/researcher/extensions/models/cve_research.ts:782-804` The comment on line 782 says `"preferring CISA KEV > NVD > GitHub"`, but the iteration order is `[...ghResult.findings, ...nvdResult.findings, ...kevResult.findings]`. The first occurrence wins for all base fields (description, severity, severityScore, source, published). Only `knownExploited` and `categories` are merged from later duplicates. **Breaking example:** A CVE appears in both GitHub Advisory and CISA KEV. The record is stored with `source: "GITHUB"`, GitHub's description, and GitHub's severity score. The `knownExploited` flag is correctly merged from KEV, but the `source` field will show "GitHub Advisory" in reports and Discord embeds instead of "CISA KEV". Users see a misleadingly attributed record for actively exploited vulnerabilities. **Suggested fix:** Either reverse the iteration order to `[...kevResult.findings, ...nvdResult.findings, ...ghResult.findings]` (so KEV data is stored first and kept), or update the merge logic to overwrite base fields when a higher-priority source is encountered. Or, if the current behavior is intentional, fix the comment. 2. **Discord webhook fetch has no timeout** — `cve/researcher/extensions/models/discord_webhook_env.ts:47-51` The `fetch(webhookUrl, ...)` call has no `signal: AbortSignal.timeout(...)`, unlike `fetchJson` in `cve_research.ts` which uses a 30-second timeout. If the Discord webhook endpoint hangs (DNS resolution stall, connection accepted but no response), this call blocks indefinitely. **Breaking example:** Discord experiences a partial outage where connections are accepted but responses stall. The workflow step hangs forever, preventing the workflow from completing or retrying. **Suggested fix:** Add `signal: AbortSignal.timeout(15_000)` to the fetch options, matching the pattern used in `fetchJson`. 3. **`categorizeCve` false-positive on "dos" substring** — `cve/researcher/extensions/models/cve_research.ts:241` `desc.includes("dos")` matches any description containing the substring "dos" — not just "denial of service" or "DoS". While `desc` is lowercased so proper nouns won't match, product names or technical terms containing "dos" (e.g., "msdos", "freedos", "dosfstools", "kudos") would incorrectly categorize the CVE as `"dos"`. **Breaking example:** A CVE description mentioning "FreeDOS" or "dosfstools" gets tagged `dos` when it's actually a memory corruption bug, skewing category-based filtering and Discord embed grouping. **Suggested fix:** Use word-boundary matching: `/\bdos\b/` or `/\bdenial of service\b|\bdos\b/` instead of `desc.includes("dos")`. The "denial of service" long form is already checked separately, so the short-form check could be tightened to `desc.includes(" dos ")` or a regex with word boundaries. ### Low 1. **State file write is non-atomic** — `cve/researcher/extensions/models/cve_research.ts:160-168` `Deno.writeTextFile` is not atomic. If the process crashes mid-write, `cve-research-state.json` could be corrupted. On next run, `loadState` catches the parse error and returns empty state, causing all CVEs to be re-flagged as "new". Unlikely in practice since the file is small and writes are fast, but a `write-then-rename` pattern would eliminate the risk. 2. **`seenIds` set grows without bound** — `cve/researcher/extensions/models/cve_research.ts:833-835` Every run adds new CVE IDs to `seenIds` but old IDs are never pruned. Over months of daily runs, this set grows without limit. At ~50 new IDs/day, after a year that's ~18,000 entries (~500KB JSON). Not operationally dangerous but worth noting — a periodic prune (e.g., drop IDs older than 180 days) would keep the file bounded. 3. **Non-retryable HTTP errors retried without delay** — `cve/researcher/extensions/models/cve_research.ts:193-195` In `fetchJson`, HTTP errors other than 429/503 (e.g., 400, 401, 403, 404) hit `continue` without any delay, immediately retrying. A 404 will be retried 3 times in rapid succession with no hope of success. 4. **Discord webhook has no retry mechanism** — `cve/researcher/extensions/models/discord_webhook_env.ts:47-58` Unlike the CVE source queries which retry on transient errors, the Discord webhook POST has no retry logic. A transient 429 or 503 from Discord will immediately fail the workflow step. ### Verdict **PASS** — No critical or high severity issues found. The deduplication priority mismatch (Medium #1) produces incorrect source attribution but doesn't affect the functional correctness of scoring or alerting since `knownExploited` is correctly merged. The missing timeout (Medium #2) is a robustness gap but not a correctness bug. The "dos" substring match (Medium #3) is a minor classification accuracy issue. All three mediums are worth fixing but none block the merge.
Author
Owner

Code Review

Blocking Issues

None.

Suggestions

  1. research.execute path has no test coverage (cve/researcher/extensions/models/cve_research.ts:739).
    The HTTP fetching, date-window filtering, deduplication, state load/save, and formatDiscordSummary logic are all untested. The report module (cve_research_report_test.ts) demonstrates the right approach — an in-memory makeContext mock + encoded JSON bytes. A parallel setup using Deno.serve({ port: 0 }) for mock NVD/GitHub/CISA responses would give meaningful coverage for the core pipeline. The Zod schema tests are solid but don't exercise the code that matters most at runtime.

  2. fetchJson retries non-retriable HTTP errors immediately (cve_research.ts:193).
    The 429/503 branch correctly backs off, but the generic !resp.ok branch (continue with no delay) retries 401/403/404 responses three times immediately. A 403 from NVD (bad API key) or a 404 from CISA KEV fires three requests before giving up. These errors won't succeed on retry; the behaviour is wasteful and obscures permanent failures. A break (or at minimum a short await before continue) for non-rate-limit errors would fix this.

  3. discordSummary Zod description says "under 2000 chars" (cve_research.ts:114).
    formatDiscordSummary returns JSON.stringify({ embeds: [embed] }) — a full Discord webhook payload. With three embed fields each up to 1 024 chars, plus title/description/footer, the JSON easily exceeds 2 000 chars when there are more than a handful of CVEs. The description is misleading to any consumer of this resource field. Consider updating it to something like "JSON-encoded Discord webhook payload (embeds)".

  4. error field typed as boolean instead of string in report (cve_research_report.ts:418).

    { queried?: boolean; count?: number; error?: boolean }
    

    The actual runtime value is string | undefined (matching the Zod schema in cve_research.ts). The type assertion masks this under strict mode. The truthy check src.nvd.error ? " (error)" : "" happens to work at runtime because a non-empty string is truthy, but the annotation will mislead future readers. Change error?: boolean to error?: string.

  5. GitHub Advisory cveId fallback can produce "GHSA-undefined" (cve_research.ts:386).

    const cveId: string = (adv.cve_id as string) ??
      `GHSA-${adv.ghsa_id as string}`;
    

    If both cve_id and ghsa_id are absent (or null), cveId becomes "GHSA-undefined", which will be persisted to state and stored as a resource. The GitHub Advisory API always provides ghsa_id, so this is unlikely in practice, but a guard (adv.ghsa_id ?? "unknown") would prevent silent data corruption.

## Code Review ### Blocking Issues None. ### Suggestions 1. **`research.execute` path has no test coverage** (`cve/researcher/extensions/models/cve_research.ts:739`). The HTTP fetching, date-window filtering, deduplication, state load/save, and `formatDiscordSummary` logic are all untested. The report module (`cve_research_report_test.ts`) demonstrates the right approach — an in-memory `makeContext` mock + encoded JSON bytes. A parallel setup using `Deno.serve({ port: 0 })` for mock NVD/GitHub/CISA responses would give meaningful coverage for the core pipeline. The Zod schema tests are solid but don't exercise the code that matters most at runtime. 2. **`fetchJson` retries non-retriable HTTP errors immediately** (`cve_research.ts:193`). The `429`/`503` branch correctly backs off, but the generic `!resp.ok` branch (`continue` with no delay) retries `401`/`403`/`404` responses three times immediately. A 403 from NVD (bad API key) or a 404 from CISA KEV fires three requests before giving up. These errors won't succeed on retry; the behaviour is wasteful and obscures permanent failures. A `break` (or at minimum a short `await` before `continue`) for non-rate-limit errors would fix this. 3. **`discordSummary` Zod description says "under 2000 chars"** (`cve_research.ts:114`). `formatDiscordSummary` returns `JSON.stringify({ embeds: [embed] })` — a full Discord webhook payload. With three embed fields each up to 1 024 chars, plus title/description/footer, the JSON easily exceeds 2 000 chars when there are more than a handful of CVEs. The description is misleading to any consumer of this resource field. Consider updating it to something like `"JSON-encoded Discord webhook payload (embeds)"`. 4. **`error` field typed as `boolean` instead of `string` in report** (`cve_research_report.ts:418`). ```typescript { queried?: boolean; count?: number; error?: boolean } ``` The actual runtime value is `string | undefined` (matching the Zod schema in `cve_research.ts`). The type assertion masks this under strict mode. The truthy check `src.nvd.error ? " (error)" : ""` happens to work at runtime because a non-empty string is truthy, but the annotation will mislead future readers. Change `error?: boolean` to `error?: string`. 5. **GitHub Advisory `cveId` fallback can produce `"GHSA-undefined"`** (`cve_research.ts:386`). ```typescript const cveId: string = (adv.cve_id as string) ?? `GHSA-${adv.ghsa_id as string}`; ``` If both `cve_id` and `ghsa_id` are absent (or null), `cveId` becomes `"GHSA-undefined"`, which will be persisted to state and stored as a resource. The GitHub Advisory API always provides `ghsa_id`, so this is unlikely in practice, but a guard (`adv.ghsa_id ?? "unknown"`) would prevent silent data corruption.
stack72 deleted branch cve-scanner 2026-06-25 19:27:31 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
swamp-club/swamp-extensions!75
No description provided.