From fbdf05392a9fab9641361ad9a7231546b7c47c45 Mon Sep 17 00:00:00 2001 From: jramos Date: Fri, 30 Jan 2026 15:11:30 -0700 Subject: [PATCH] Add NVD sync implementation plan Detailed plan for the NVD lookup + retroactive sync feature covering stash resolution, backend endpoints, frontend NvdSyncModal component, and App.js integration. Note: claude_status.md is gitignored but has been updated locally with full session context including stash state, conflict resolution steps, and task list. Copy it manually to the offsite machine if needed. Co-Authored-By: Claude Opus 4.5 --- plan.md | 297 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 plan.md diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..d2cdbc9 --- /dev/null +++ b/plan.md @@ -0,0 +1,297 @@ +# NVD Lookup + Retroactive Sync — Implementation Plan + +## Overview +Two capabilities on `feature/nvd-lookup` branch: +1. **Auto-fill on Add CVE** (DONE, stashed) — onBlur NVD lookup fills description/severity/date in the Add CVE modal +2. **Sync with NVD** (TO DO) — bulk tool for editors/admins to retroactively update existing CVE entries from NVD, with per-CVE choice to keep or replace description + +## Current State + +### Git State +- **Branch:** `feature/nvd-lookup` (branched from master post-audit-merge) +- **Stash:** `stash@{0}` contains the auto-fill implementation (4 files) +- **Master** now has audit logging (merged from feature/audit on 2026-01-30) +- Offsite repo is up to date through the feature/audit merge to master + +### What's in the Stash +The stash contains working NVD auto-fill code that needs to be popped and conflict-resolved before continuing: + +**`backend/routes/nvdLookup.js` (NEW file)** +- Factory function: `createNvdLookupRouter(db, requireAuth)` +- `GET /lookup/:cveId` endpoint +- Validates CVE ID format (regex: `CVE-YYYY-NNNNN`) +- Calls `https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=...` +- 10-second timeout via `AbortSignal.timeout(10000)` +- Optional `apiKey` header from `NVD_API_KEY` env var +- CVSS severity cascade: v3.1 → v3.0 → v2.0 +- Maps NVD uppercase severity to app format (CRITICAL→Critical, etc.) +- Returns: `{ description, severity, published_date }` + +**`backend/server.js` (MODIFIED)** +- Adds `const createNvdLookupRouter = require('./routes/nvdLookup');` +- Adds `app.use('/api/nvd', createNvdLookupRouter(db, requireAuth));` + +**`frontend/src/App.js` (MODIFIED)** +- New state: `nvdLoading`, `nvdError`, `nvdAutoFilled` +- New function: `lookupNVD(cveId)` — calls backend, auto-fills form fields +- CVE ID input: `onBlur` triggers lookup, `onChange` resets NVD feedback +- Spinner (Loader icon) in CVE ID field while loading +- Green "Auto-filled from NVD" with CheckCircle on success +- Amber warning with AlertCircle on errors (non-blocking) +- Description only fills if currently empty; severity + published_date always update +- NVD state resets on modal close (X, Cancel) and form submit + +**`backend/.env.example` (MODIFIED)** +- Adds `NVD_API_KEY=` with comment about rate limits + +### Stash Conflict Resolution +Popping the stash will conflict in `server.js` because master now has audit imports that didn't exist when the stash was created. Resolution: + +The conflict is in the imports section. Keep ALL existing audit lines from master: +```js +const createAuditLogRouter = require('./routes/auditLog'); +const logAudit = require('./helpers/auditLog'); +``` +AND add the NVD line: +```js +const createNvdLookupRouter = require('./routes/nvdLookup'); +``` + +Similarly, keep the audit route mount and add the NVD mount after it: +```js +app.use('/api/audit-logs', createAuditLogRouter(db, requireAuth, requireRole)); +app.use('/api/nvd', createNvdLookupRouter(db, requireAuth)); +``` + +Then `git add backend/server.js` to mark resolved and `git stash drop`. + +--- + +## Step 1: Resolve Stash + Rebase onto Master + +```bash +git checkout feature/nvd-lookup +git rebase master # Get audit changes into the branch +git stash pop # Apply NVD changes (will conflict in server.js) +# Resolve conflict in server.js as described above +git add backend/server.js +git stash drop +``` + +Verify: `backend/routes/nvdLookup.js` exists, `server.js` has both audit AND NVD imports/mounts. + +--- + +## Step 2: Backend — New Endpoints in `server.js` + +### 2A: `GET /api/cves/distinct-ids` +Place BEFORE `GET /api/cves/check/:cveId` (to avoid route param conflict): +```js +app.get('/api/cves/distinct-ids', requireAuth(db), (req, res) => { + db.all('SELECT DISTINCT cve_id FROM cves ORDER BY cve_id', [], (err, rows) => { + if (err) return res.status(500).json({ error: err.message }); + res.json(rows.map(r => r.cve_id)); + }); +}); +``` + +### 2B: `POST /api/cves/nvd-sync` +Place after the existing `PATCH /api/cves/:cveId/status`: +```js +app.post('/api/cves/nvd-sync', requireAuth(db), requireRole('editor', 'admin'), (req, res) => { + const { updates } = req.body; + if (!Array.isArray(updates) || updates.length === 0) { + return res.status(400).json({ error: 'No updates provided' }); + } + + let updated = 0; + const errors = []; + let completed = 0; + + db.serialize(() => { + updates.forEach((entry) => { + const fields = []; + const values = []; + if (entry.description !== null && entry.description !== undefined) { + fields.push('description = ?'); + values.push(entry.description); + } + if (entry.severity !== null && entry.severity !== undefined) { + fields.push('severity = ?'); + values.push(entry.severity); + } + if (entry.published_date !== null && entry.published_date !== undefined) { + fields.push('published_date = ?'); + values.push(entry.published_date); + } + if (fields.length === 0) { + completed++; + if (completed === updates.length) sendResponse(); + return; + } + fields.push('updated_at = CURRENT_TIMESTAMP'); + values.push(entry.cve_id); + + db.run( + `UPDATE cves SET ${fields.join(', ')} WHERE cve_id = ?`, + values, + function(err) { + if (err) { + errors.push({ cve_id: entry.cve_id, error: err.message }); + } else { + updated += this.changes; + } + completed++; + if (completed === updates.length) sendResponse(); + } + ); + }); + }); + + function sendResponse() { + logAudit(db, { + userId: req.user.id, + username: req.user.username, + action: 'cve_nvd_sync', + entityType: 'cve', + entityId: null, + details: { count: updated, cve_ids: updates.map(u => u.cve_id) }, + ipAddress: req.ip + }); + const result = { message: 'NVD sync completed', updated }; + if (errors.length > 0) result.errors = errors; + res.json(result); + } +}); +``` + +**How "keep existing description" works:** If the user chooses to keep the existing description, the frontend sends `description: null` for that CVE. The backend skips null fields, so the description is not overwritten. Severity and published_date are always sent (auto-update). + +--- + +## Step 3: Frontend — New `NvdSyncModal.js` Component + +**File:** `frontend/src/components/NvdSyncModal.js` + +### Props +```jsx + +``` + +### Phase Machine +| Phase | What's shown | +|-------|-------------| +| `idle` | CVE count + "Fetch NVD Data" button | +| `fetching` | Progress bar, current CVE being fetched, cancel button | +| `review` | Comparison table with per-CVE description choice | +| `applying` | Spinner | +| `done` | Summary (X updated, Y errors) + Close button | + +### Fetching Logic +- Iterate CVE IDs sequentially +- Call `GET /api/nvd/lookup/:cveId` for each +- 7-second delay between requests (safe for 5 req/30s without API key) +- On 429: wait 35 seconds, retry once +- On 404: mark as "Not found in NVD" (gray, skipped) +- On timeout/error: mark with warning (skipped) +- Support cancellation via AbortController + +### Comparison Table Columns +| Column | Content | +|--------|---------| +| CVE ID | The identifier | +| Status | Icon: check=found, warning=error, dash=no changes | +| Severity | `[Current] → [NVD]` with color badges, or "No change" | +| Published Date | `Current → NVD` or "No change" | +| Description | Truncated preview with expand toggle. Current (red bg) vs NVD (green bg) when different | +| Choice | Radio: "Keep existing" (default) / "Use NVD" — only shown when descriptions differ | + +### Bulk Controls +Above the table: +- Summary: `Found: N | Up to date: N | Changes: N | Not in NVD: N | Errors: N` +- Bulk toggle: "Keep All Existing" / "Use All NVD Descriptions" + +Below the table: +- "Apply N Changes" button (count updates dynamically) +- "Cancel" button + +### Apply Logic +Build updates array: +- For each CVE with NVD data (no error): + - Always include `severity` and `published_date` if different from current + - Include `description` only if user chose "Use NVD" — otherwise send `null` + - Skip CVEs where nothing changed +- POST to `/api/cves/nvd-sync` +- On success: call `onSyncComplete()` to refresh CVE list, then show done phase + +--- + +## Step 4: Frontend — App.js Integration + +Minimal changes following `AuditLog`/`UserManagement` pattern: + +1. **Import:** Add `NvdSyncModal` and `RefreshCw` icon +2. **State:** Add `const [showNvdSync, setShowNvdSync] = useState(false);` +3. **Header button** (next to "Add CVE/Vendor", visible to editors/admins): +```jsx +{canWrite() && ( + +)} +``` +4. **Modal render** (alongside other modals): +```jsx +{showNvdSync && ( + setShowNvdSync(false)} onSyncComplete={() => fetchCVEs()} /> +)} +``` + +--- + +## Step 5: AuditLog Badge + +**File:** `frontend/src/components/AuditLog.js` + +Add to the `ACTION_BADGES` object: +```js +cve_nvd_sync: { bg: 'bg-green-100', text: 'text-green-800' }, +``` + +--- + +## Step 6: .env.example (already in stash) +``` +# NVD API Key (optional - increases rate limit from 5 to 50 requests per 30s) +# Request one at https://nvd.nist.gov/developers/request-an-api-key +NVD_API_KEY= +``` + +--- + +## File Summary + +| File | Action | Lines Changed (est.) | +|------|--------|---------------------| +| `backend/server.js` | Modify | +40 (NVD mount + 2 new endpoints) | +| `backend/routes/nvdLookup.js` | From stash | 0 (already complete) | +| `backend/.env.example` | From stash | +3 | +| `frontend/src/components/NvdSyncModal.js` | New | ~350-400 | +| `frontend/src/App.js` | Modify | +10 (import, state, button, modal) | +| `frontend/src/components/AuditLog.js` | Modify | +1 (badge entry) | + +--- + +## Verification Checklist +1. Pop stash, resolve conflict, verify `nvdLookup.js` and server.js are correct +2. Test NVD lookup via curl: `curl -b cookie.txt http://localhost:3001/api/nvd/lookup/CVE-2024-3094` +3. Test distinct-ids: `curl -b cookie.txt http://localhost:3001/api/cves/distinct-ids` +4. Open Add CVE modal, type CVE ID, tab out → verify auto-fill works +5. Click "Sync with NVD" button → modal opens with CVE count +6. Click "Fetch NVD Data" → progress bar, rate-limited fetching +7. Review comparison table → verify diffs shown correctly +8. Toggle description choices, click "Apply" → verify database updated +9. Confirm main CVE list refreshes with new data +10. Check audit log for `cve_nvd_sync` entry