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