Files
cve-dashboard/.kiro/specs/cve-tooltip-hover/design.md

230 lines
11 KiB
Markdown
Raw Normal View 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
```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