Problem Statement
CVE Search already covers search, triage, watchlists, alerts, projects, and repository scanning — but several gaps stop it from being genuinely useful as a day-to-day analyst tool:
- Alerting doesn't alert. Digest deliveries to webhook/Slack/email destinations are recorded as "sent" but no message ever leaves the server, and nothing runs on a schedule — an analyst must manually trigger digest runs. An alerting system that requires polling by hand defeats its purpose.
- The app's own security audit found unfixed issues. The fix-generation endpoint accepts client-supplied AI API keys (critical), a schema-migration helper interpolates identifiers into SQL, session cookies lack the Secure flag, responses lack security headers, and error responses can leak internals. These block any networked or team deployment.
- Repository scanning covers too few ecosystems. Only npm/pnpm and Composer are supported. Teams with yarn, Python, Go, or Rust projects get zero coverage and silently believe they are scanned.
- The app goes dark when the upstream CVE API is down. CVE data is already persisted locally on every fetch, but search and detail pages fail outright on upstream outages instead of serving cached records.
Solution
Make the existing workflows trustworthy and complete rather than adding new surface area:
- Digests are actually delivered to webhook and Slack destinations with honest sent/failed status, and a background scheduler runs due digests and alert evaluations automatically.
- All security-audit findings rated Critical/High (except full multi-user authentication) plus error-message sanitization are fixed, so the app is safe to run on a network.
- Dependency scanning gains yarn, Python (PyPI), Go, and Rust (crates.io) support using the same OSV-based pipeline, and the UI reflects exactly which file types are covered.
- Search and CVE detail degrade gracefully to the local CVE store during upstream outages, with a visible cached-data indicator.
User Stories
Security hardening
- As an administrator, I want all AI provider credentials to come exclusively from server-side configuration, so that no client can inject its own API key or redirect AI traffic to a hostile endpoint.
- As an administrator, I want the fix-generation endpoint to ignore client-supplied API keys entirely, so that admin-configured AI policy cannot be bypassed from the browser.
- As an administrator, I want schema-migration helpers to validate table and column identifiers before building DDL statements, so that no code path can assemble ALTER TABLE statements from unvalidated strings.
- As a team operating the app over HTTPS, I want session cookies marked Secure, HttpOnly, and SameSite, so that session identifiers cannot leak over plaintext connections or cross-site requests.
- As an administrator, I want standard security headers (X-Content-Type-Options, Referrer-Policy, frame-ancestors protection, Permissions-Policy, and a report-only Content-Security-Policy) on every response, so that the app is hardened against clickjacking and MIME sniffing.
- As an administrator, I want 5xx responses to carry a generic client-facing message while full details go to server logs, so that stack traces and internal paths never leak to clients.
Real alerting
- As a security analyst, I want digests for webhook destinations to be POSTed as JSON to the configured URL, so that my team receives vulnerability digests in our own tooling instead of only inside the app.
- As a security analyst, I want digests for Slack destinations to arrive as formatted Slack messages via the incoming-webhook URL, so that alerts surface where my team already works.
- As a security analyst, I want each delivery record to show sent or failed with the failure reason, so that I can trust the delivery history instead of a fake "sent" status.
- As a security analyst, I want failed deliveries to be retried on the next scheduled run, so that transient outages do not silently drop digests.
- As a security analyst, I want due digests and alert-rule evaluations to run automatically in the background, so that the app alerts me without anyone manually triggering a run.
- As an administrator, I want the background scheduler controlled by environment configuration (enabled flag and interval), so that I can disable it where an external cron drives the trigger endpoint instead.
- As a security analyst, I want an in-app notification recorded for every digest run regardless of channel, so that the in-app audit trail stays complete.
- As an administrator, I want outbound digest destinations restricted to HTTPS URLs, so that digest content cannot be exfiltrated over plaintext or pointed at obviously unsafe targets.
Broader ecosystem scanning
- As a developer monitoring repositories, I want yarn.lock files parsed, so that yarn-based projects get the same vulnerability coverage as npm and pnpm projects.
- As a developer, I want requirements.txt files with pinned versions scanned against the PyPI ecosystem, so that Python services are covered.
- As a developer, I want poetry.lock and uv.lock files scanned, so that modern Python dependency managers get lockfile-accurate coverage.
- As a developer, I want go.mod files scanned against the Go ecosystem, so that Go services are covered.
- As a developer, I want Cargo.lock files scanned against the crates.io ecosystem, so that Rust projects are covered.
- As a developer with a monorepo, I want new ecosystem files discovered in subdirectories exactly like existing ones, so that coverage does not depend on repository layout.
- As a developer, I want the supported-ecosystems list shown in the repos UI to reflect every newly supported file type, so that I know precisely what scanning covers and what it does not.
Offline resilience
- As a security analyst, I want CVE detail pages to fall back to the locally cached record when the upstream API fails or times out, so that I can keep working during outages.
- As a security analyst, I want search to fall back to the local CVE store when upstream is unavailable, so that prioritization work continues with the data already gathered.
- As a security analyst, I want cached fallback responses visibly marked as cached data with their stored timestamp, so that I never mistake stale data for live data.
Implementation Decisions
- Fix-route AI settings: the request payload no longer carries an API key in any form; the settings resolver stops accepting keys from request-derived input. Client-supplied provider/model overrides are honored only when they match a server-configured provider. This brings the fix route in line with every other AI route, which already reads credentials from environment variables only.
- Schema-migration identifier guard: the column-ensuring helper validates table and column names against a strict identifier pattern (letters, digits, underscore, not starting with a digit) and rejects anything else. Identifiers remain internal constants; the guard is defense in depth.
- Session cookie: HttpOnly and SameSite=Lax always; Secure when the request arrives over HTTPS or when an environment flag forces it (for proxied deployments).
- Security headers: applied globally via framework-level header configuration. Enforced: X-Content-Type-Options, Referrer-Policy, X-Frame-Options/frame-ancestors, Permissions-Policy. Content-Security-Policy ships in report-only mode first to avoid breaking framework inline scripts and Radix styling.
- Error sanitization: the shared route-protection wrapper becomes the single place that maps thrown errors to client responses; clients receive the route's configured generic message, full error details are logged server-side.
- Digest delivery transport: delivery happens inside the existing due-digest run. Webhook destinations receive the digest as a JSON POST; Slack destinations receive a Slack-compatible message payload to the incoming-webhook URL. All outbound calls use the established AbortController timeout pattern. Delivery status becomes a real state: sent, failed (with error detail), or preview. A failed delivery leaves the digest due so the next run retries it. Destination URLs must be HTTPS. Email remains record-only (out of scope below).
- Background scheduler: a single exported tick function runs due digests for all users and evaluates alert rules; the framework instrumentation hook starts an interval that calls it. Environment variables control enablement and interval. The tick is idempotent and guarded against overlapping runs. The existing manual trigger endpoint remains for external cron setups.
- New ecosystems: the dependency parser gains yarn.lock (npm ecosystem), requirements.txt/poetry.lock/uv.lock (PyPI), go.mod (Go), and Cargo.lock (crates.io). Each follows the existing precedence rules: lockfile-pinned versions preferred, manifest declarations used with the existing version-normalization behavior otherwise. Discovery reuses the existing tree-walk that already finds dependency files in subdirectories. The supported-ecosystems registry is the single source of truth the UI renders, so new file types appear there automatically.
- Offline fallback: the vulnerability API layer catches upstream failure (timeout, 5xx, network error) and consults the local CVE store, which is already populated on every successful upstream fetch. Fallback responses carry a source marker (cache vs live) and the stored-at timestamp; search and detail surfaces render a cached-data indicator from that marker. Upstream 404s for unknown CVEs are not masked by cache fallback.
Testing Decisions
- Approach: strict TDD, red-green-refactor, vertical slices — one failing test, minimal code to pass, repeat. Tests verify externally observable behavior through public interfaces, never internal structure.
- Test runner: the existing setup — node:test with node:assert/strict, TypeScript compiled via the dedicated test tsconfig, run with
npm test.
- Seams (all existing patterns in the test suite):
- Library module public functions, with temporary SQLite databases via the
DATABASE_FILE environment variable (prior art: the store tests and route-guard test).
- Route handlers invoked directly with constructed
Request objects through the route-protection wrapper (prior art: the route-guard test).
- Upstream HTTP mocked by swapping
globalThis.fetch (prior art: the GitHub client tests).
- One new thin seam: the scheduler's exported tick function is tested directly; the interval wiring in the instrumentation hook stays untested glue.
- What gets tested per theme: security fixes assert response-level behavior (ignored client keys, rejected identifiers, cookie attributes, header presence, sanitized error bodies); delivery asserts the outbound payload, status transitions, and retry-on-next-run behavior; parsers assert extracted name/version/ecosystem tuples from fixture files (prior art: existing parser tests and fixtures directory); fallback asserts cache-served responses and source markers under a failing fetch.
Out of Scope
- Email/SMTP digest delivery (stays record-only).
- Java ecosystems (Maven/Gradle) and other additional ecosystems beyond yarn, Python, Go, Rust.
- Full multi-user authentication (audit finding H4) — the session model stays as-is.
- Enforced Content-Security-Policy (report-only first; enforcement is follow-up work).
- Browser/E2E test infrastructure.
- Queue or worker infrastructure for delivery — the in-process scheduler and existing trigger endpoint suffice.
- Moving the data directory or deployment-specific reverse-proxy hardening.
Further Notes
- The security items come directly from the project's own security audit document (dated 2026-03-08); this PRD covers its priority items 1–4 and 6–7.
- KEV and EPSS enrichment already exist and are not part of this PRD.
- Implementation order should lead with the critical security fix (client-supplied AI keys), then alerting, since those change trust-relevant behavior; ecosystem parsers are independent and parallelizable.
Problem Statement
CVE Search already covers search, triage, watchlists, alerts, projects, and repository scanning — but several gaps stop it from being genuinely useful as a day-to-day analyst tool:
Solution
Make the existing workflows trustworthy and complete rather than adding new surface area:
User Stories
Security hardening
Real alerting
Broader ecosystem scanning
Offline resilience
Implementation Decisions
Testing Decisions
npm test.DATABASE_FILEenvironment variable (prior art: the store tests and route-guard test).Requestobjects through the route-protection wrapper (prior art: the route-guard test).globalThis.fetch(prior art: the GitHub client tests).Out of Scope
Further Notes