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

230 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```mermaid
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)**:
```json
{
"exists": true,
"cve_id": "CVE-2024-12345",
"description": "A vulnerability in...",
"severity": "High"
}
```
- **Response (not found)**:
```json
{ "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.
```sql
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
```javascript
// 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