Files
cve-dashboard/.kiro/specs/cve-tooltip-hover/design.md
jramos 9b36a58959 feat: add CVE tooltip on hover in Reporting Page
- Add GET /api/cves/:cveId/tooltip backend endpoint with description truncation
- Create CveTooltip portal component with caching, severity badges, and viewport-aware positioning
- Integrate tooltip into ReportingPage with 300ms hover delay on CVE badge spans
2026-04-09 14:42:23 -06:00

11 KiB
Raw Permalink Blame History

Design Document: CVE Tooltip Hover

Overview

This feature adds a hover tooltip to CVE badges in the Reporting Page findings table. When a user pauses their cursor over a CVE identifier badge, the system fetches a brief description and severity from the backend and displays it in a styled floating tooltip. Responses are cached in-memory to avoid redundant API calls, and a 300ms hover delay prevents tooltip flicker during fast mouse movement.

The implementation spans two layers:

  1. A new lightweight backend endpoint (/api/cves/:cveId/tooltip) that queries the existing cves SQLite table and returns a trimmed response.
  2. A frontend CveTooltip component rendered via a React portal, with an in-memory cache (React ref), hover delay timer, and viewport-aware positioning.

Architecture

sequenceDiagram
    participant User
    participant CVEBadge as CVE Badge (ReportingPage)
    participant Tooltip as CveTooltip Component
    participant Cache as Tooltip Cache (useRef)
    participant API as /api/cves/:cveId/tooltip
    participant DB as SQLite (cves table)

    User->>CVEBadge: mouseenter
    CVEBadge->>Tooltip: start 300ms delay timer
    Note over Tooltip: If mouseout before 300ms, cancel

    alt Cache hit
        Tooltip->>Cache: lookup(cveId)
        Cache-->>Tooltip: cached data
        Tooltip->>User: show tooltip (or skip if exists:false)
    else Cache miss
        Tooltip->>API: GET /api/cves/:cveId/tooltip
        API->>DB: SELECT cve_id, description, severity FROM cves WHERE cve_id = ?
        DB-->>API: row or null
        API-->>Tooltip: { exists, cve_id, description, severity }
        Tooltip->>Cache: store response
        Tooltip->>User: show tooltip (or skip if exists:false)
    end

    User->>CVEBadge: mouseleave
    CVEBadge->>Tooltip: hide + clear timer

Key Design Decisions

  1. Inline endpoint in server.js — The tooltip endpoint is a single GET route on the existing /api/cves path prefix. It follows the pattern of other simple CVE endpoints already defined inline in server.js (e.g., /api/cves/check/:cveId, /api/cves/:cveId/vendors). No separate route module needed.

  2. React portal for tooltip rendering — The tooltip is rendered via ReactDOM.createPortal to document.body, avoiding overflow/clipping issues from the table's scroll container. The ReportingPage already imports ReactDOM for other portal usage.

  3. useRef for cache instead of useState — The cache is a plain Map stored in a useRef. This avoids re-renders when cache entries are added and persists across renders without triggering updates. The cache is cleared when the findings data is re-synced.

  4. Single shared tooltip instance — Only one tooltip is visible at a time. The parent component tracks which CVE badge is hovered and passes the active CVE ID + badge position to the tooltip component.

Components and Interfaces

Backend

GET /api/cves/:cveId/tooltip

Added inline in server.js alongside existing CVE endpoints.

  • Auth: requireAuth(db) — session cookie required
  • Params: :cveId — validated against CVE_ID_PATTERN (/^CVE-\d{4}-\d{4,}$/)
  • Query: SELECT cve_id, description, severity FROM cves WHERE cve_id = ? LIMIT 1
  • Response (found):
    {
      "exists": true,
      "cve_id": "CVE-2024-12345",
      "description": "A vulnerability in...",
      "severity": "High"
    }
    
  • Response (not found):
    { "exists": false }
    
  • Description truncation: If description.length > 300, return description.substring(0, 300) + '…'

Frontend

CveTooltip Component (new file: frontend/src/components/CveTooltip.js)

A portal-rendered tooltip that receives positioning data and CVE info.

Props:

Prop Type Description
cveId string | null The CVE ID to display. null hides the tooltip.
anchorRect DOMRect | null Bounding rect of the hovered badge for positioning.
cache React.MutableRefObject<Map> Shared cache ref from parent.

Internal state:

  • data — fetched tooltip payload ({ exists, cve_id, description, severity } or null)
  • loading — boolean, true while fetch is in-flight

Behavior:

  1. When cveId changes to a non-null value, check cache.current for the CVE ID.
  2. If cached and exists: false, render nothing.
  3. If cached and exists: true, display immediately.
  4. If not cached, set loading = true, fetch from API, store result in cache, set loading = false.
  5. Position the tooltip above the badge by default. If the tooltip would overflow the top of the viewport, position it below instead.
  6. Render via ReactDOM.createPortal to document.body.

ReportingPage Integration

Modifications to the existing renderCell function for the 'cves' case:

  • Add onMouseEnter / onMouseLeave handlers to each CVE badge <span>.
  • onMouseEnter: Start a 300ms setTimeout. On fire, set active CVE ID + badge getBoundingClientRect() into state.
  • onMouseLeave: Clear the timeout. Set active CVE ID to null.
  • Render a single <CveTooltip> instance at the bottom of the component, passing the active CVE ID, anchor rect, and cache ref.
  • On data sync (when findings are refreshed), call cache.current.clear().

Data Models

Existing: cves Table (SQLite)

The tooltip endpoint queries the existing table. No schema changes required.

CREATE TABLE cves (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    cve_id TEXT NOT NULL,
    vendor TEXT NOT NULL,
    severity TEXT CHECK(severity IN ('Critical', 'High', 'Medium', 'Low')),
    description TEXT,
    published_date TEXT,
    status TEXT DEFAULT 'Open',
    created_by INTEGER,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(cve_id, vendor)
);

The query uses LIMIT 1 since a CVE may have multiple vendor rows — the description and severity from any row suffice for the tooltip blurb.

Frontend Cache Structure

// cache.current is a Map<string, object>
// Key: CVE ID string (e.g. "CVE-2024-12345")
// Value: API response object
//   { exists: false }
//   OR
//   { exists: true, cve_id: string, description: string, severity: string }

Correctness Properties

A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.

Property 1: Tooltip endpoint returns correct data for existing CVEs

For any CVE record inserted into the cves table with a valid cve_id, description, and severity, a GET request to /api/cves/:cveId/tooltip SHALL return { exists: true } with the matching cve_id and severity, and a description that is either the original (if ≤ 300 chars) or truncated to 300 chars + ellipsis.

Validates: Requirements 1.1, 1.3, 1.5

Property 2: Description truncation preserves content and enforces length

For any string of arbitrary length, the truncation function SHALL return the original string unchanged if its length is ≤ 300, or return exactly the first 300 characters followed by "…" if its length exceeds 300. In both cases, the output starts with the same characters as the input.

Validates: Requirements 1.5

Property 3: Tooltip positioning flips based on available viewport space

For any anchor rectangle position and viewport height, the tooltip SHALL be positioned above the anchor when anchorRect.top provides sufficient space for the tooltip height, and below the anchor otherwise. The tooltip SHALL never overflow the top or bottom of the viewport.

Validates: Requirements 3.1, 3.2

Property 4: Cache round-trip — fetch then cache-hit avoids network call

For any CVE ID, after the tooltip system fetches data from the API and stores it in the cache, a subsequent tooltip request for the same CVE ID SHALL return the identical cached data object without making an additional network request.

Validates: Requirements 4.1, 4.2

Error Handling

Scenario Layer Behavior
Invalid CVE ID format in URL param Backend Return 400 { error: 'Invalid CVE ID format.' }
Database query error Backend Log error, return 500 { error: 'Internal server error.' }
No session cookie / expired session Backend requireAuth middleware returns 401
Network error during fetch Frontend Catch error, hide tooltip (do not cache failures), log to console
Fetch timeout / slow response Frontend Show loading state; if user moves away, cancel via AbortController
Component unmounts during fetch Frontend AbortController signal aborts in-flight request, no state update

Key principle: Transient errors (network failures, timeouts) are NOT cached. Only successful API responses (both exists: true and exists: false) are stored in the cache. This ensures a retry on next hover for failed requests.

Testing Strategy

Unit Tests (Example-Based)

Test Validates
Endpoint returns { exists: false } for unknown CVE ID Req 1.2
Endpoint returns 401 without session cookie Req 1.4
Endpoint returns 400 for malformed CVE ID (e.g. "not-a-cve") Req 1.1 (error path)
Tooltip appears after 300ms hover delay Req 5.1
Tooltip cancelled if mouseout before 300ms Req 5.2
Tooltip hidden on mouseleave Req 2.2
Loading indicator shown while fetching Req 2.5
No tooltip shown when API returns exists: false Req 2.6
Severity badge uses correct color per level Req 2.4
Tooltip has max-width of 320px Req 3.3
Tooltip includes directional arrow element Req 3.5
Cache cleared on data sync/refresh Req 4.4
Cached exists: false suppresses tooltip and API call Req 4.3

Property-Based Tests

Property-based tests use fast-check (JavaScript PBT library, already compatible with the Jest/react-scripts test runner).

Each property test runs a minimum of 100 iterations.

Property Tag Focus
Property 1 Feature: cve-tooltip-hover, Property 1: Tooltip endpoint returns correct data for existing CVEs Generate random CVE records (varying description lengths 01000, all 4 severity levels), insert into test DB, call endpoint, verify response shape and truncation
Property 2 Feature: cve-tooltip-hover, Property 2: Description truncation preserves content and enforces length Generate random strings of length 02000, apply truncation function, verify length invariant and prefix preservation
Property 3 Feature: cve-tooltip-hover, Property 3: Tooltip positioning flips based on available viewport space Generate random anchorRect.top (02000), tooltip height (50200), viewport height (4001200), verify position is within viewport bounds
Property 4 Feature: cve-tooltip-hover, Property 4: Cache round-trip Generate random CVE IDs and response payloads, store in cache Map, verify subsequent lookups return identical objects and no fetch is triggered

Test Configuration

  • Test runner: react-scripts test (Jest) — already configured in the project
  • PBT library: fast-check — install via npm install --save-dev fast-check in the frontend/ directory
  • Backend endpoint tests: Use supertest or direct handler invocation with a test SQLite DB
  • Frontend component tests: React Testing Library with mocked fetch