import React, { useState, useEffect, useRef } from 'react'; import { X, Loader, AlertCircle, CheckCircle, RefreshCw, ChevronDown, ChevronUp } from 'lucide-react'; const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; const FETCH_DELAY_MS = 7000; // 7 seconds between requests (safe for 5 req/30s without API key) const RETRY_DELAY_MS = 35000; // Wait 35 seconds on 429 before retry export default function NvdSyncModal({ onClose, onSyncComplete }) { const [phase, setPhase] = useState('idle'); // idle, fetching, review, applying, done const [cveIds, setCveIds] = useState([]); const [fetchProgress, setFetchProgress] = useState({ current: 0, total: 0, currentId: '' }); const [results, setResults] = useState({}); // { cveId: { nvd: {...}, current: {...}, status: 'found'|'not_found'|'error'|'no_change', error: '' } } const [descriptionChoices, setDescriptionChoices] = useState({}); // { cveId: 'keep' | 'nvd' } const [applyResult, setApplyResult] = useState(null); const [expandedDesc, setExpandedDesc] = useState({}); const abortRef = useRef(null); // Fetch distinct CVE IDs on mount useEffect(() => { (async () => { try { const response = await fetch(`${API_BASE}/cves/distinct-ids`, { credentials: 'include' }); if (!response.ok) throw new Error('Failed to fetch CVE list'); const data = await response.json(); setCveIds(data); } catch (err) { console.error('Error fetching CVE IDs:', err); } })(); }, []); const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); const fetchNvdData = async () => { setPhase('fetching'); const controller = new AbortController(); abortRef.current = controller; const newResults = {}; setFetchProgress({ current: 0, total: cveIds.length, currentId: '' }); // First fetch current data for all CVEs let currentData = {}; try { const response = await fetch(`${API_BASE}/cves`, { credentials: 'include', signal: controller.signal }); if (response.ok) { const allCves = await response.json(); // Group by cve_id, take first entry for description/severity/date allCves.forEach(cve => { if (!currentData[cve.cve_id]) { currentData[cve.cve_id] = { description: cve.description, severity: cve.severity, published_date: cve.published_date }; } }); } } catch (err) { if (err.name === 'AbortError') { setPhase('idle'); return; } } for (let i = 0; i < cveIds.length; i++) { if (controller.signal.aborted) break; const cveId = cveIds[i]; setFetchProgress({ current: i + 1, total: cveIds.length, currentId: cveId }); try { let response = await fetch(`${API_BASE}/nvd/lookup/${encodeURIComponent(cveId)}`, { credentials: 'include', signal: controller.signal }); // Handle rate limit with one retry if (response.status === 429) { await sleep(RETRY_DELAY_MS); if (controller.signal.aborted) break; response = await fetch(`${API_BASE}/nvd/lookup/${encodeURIComponent(cveId)}`, { credentials: 'include', signal: controller.signal }); } if (response.status === 404) { newResults[cveId] = { status: 'not_found', current: currentData[cveId] || {} }; } else if (!response.ok) { const data = await response.json().catch(() => ({})); newResults[cveId] = { status: 'error', error: data.error || `HTTP ${response.status}`, current: currentData[cveId] || {} }; } else { const nvd = await response.json(); const current = currentData[cveId] || {}; const descChanged = nvd.description && nvd.description !== current.description; const sevChanged = nvd.severity && nvd.severity !== current.severity; const dateChanged = nvd.published_date && nvd.published_date !== current.published_date; if (!descChanged && !sevChanged && !dateChanged) { newResults[cveId] = { status: 'no_change', nvd, current }; } else { newResults[cveId] = { status: 'found', nvd, current, descChanged, sevChanged, dateChanged }; } } } catch (err) { if (err.name === 'AbortError') break; newResults[cveId] = { status: 'error', error: err.message, current: currentData[cveId] || {} }; } // Update results progressively setResults({ ...newResults }); // Rate limit delay (skip after last item) if (i < cveIds.length - 1 && !controller.signal.aborted) { await sleep(FETCH_DELAY_MS); } } if (!controller.signal.aborted) { setResults({ ...newResults }); // Default all description choices to 'keep' const choices = {}; Object.entries(newResults).forEach(([id, r]) => { if (r.status === 'found' && r.descChanged) { choices[id] = 'keep'; } }); setDescriptionChoices(choices); setPhase('review'); } }; const cancelFetch = () => { if (abortRef.current) abortRef.current.abort(); setPhase('idle'); }; const setBulkDescriptionChoice = (choice) => { const newChoices = {}; Object.keys(descriptionChoices).forEach(id => { newChoices[id] = choice; }); setDescriptionChoices(newChoices); }; const getChangesCount = () => { let count = 0; Object.entries(results).forEach(([id, r]) => { if (r.status === 'found') { if (r.sevChanged || r.dateChanged || (r.descChanged && descriptionChoices[id] === 'nvd')) { count++; } } }); return count; }; const applyChanges = async () => { setPhase('applying'); const updates = []; Object.entries(results).forEach(([cveId, r]) => { if (r.status !== 'found') return; const update = { cve_id: cveId }; let hasChange = false; if (r.sevChanged) { update.severity = r.nvd.severity; hasChange = true; } if (r.dateChanged) { update.published_date = r.nvd.published_date; hasChange = true; } if (r.descChanged && descriptionChoices[cveId] === 'nvd') { update.description = r.nvd.description; hasChange = true; } if (hasChange) updates.push(update); }); if (updates.length === 0) { setApplyResult({ updated: 0, message: 'No changes to apply' }); setPhase('done'); return; } try { const response = await fetch(`${API_BASE}/cves/nvd-sync`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ updates }) }); if (!response.ok) { const data = await response.json().catch(() => ({})); throw new Error(data.error || 'Sync failed'); } const data = await response.json(); setApplyResult(data); onSyncComplete(); } catch (err) { setApplyResult({ error: err.message }); } setPhase('done'); }; const truncate = (str, len = 120) => str && str.length > len ? str.substring(0, len) + '...' : str; // Summary counts const foundCount = Object.values(results).filter(r => r.status === 'found').length; const noChangeCount = Object.values(results).filter(r => r.status === 'no_change').length; const notFoundCount = Object.values(results).filter(r => r.status === 'not_found').length; const errorCount = Object.values(results).filter(r => r.status === 'error').length; return (
Update existing CVE entries with data from the National Vulnerability Database
{cveIds.length > 0 ? <>{cveIds.length} unique CVE{cveIds.length !== 1 ? 's' : ''} in database> : 'Loading CVE count...'}
This will fetch data from NVD for each CVE and let you review changes before applying. Rate-limited to stay within NVD API limits.
Fetching CVE {fetchProgress.current} of {fetchProgress.total}
{fetchProgress.currentId}
{truncate(r.nvd.description)}
)} {/* Description choice */}Applying changes...
Sync failed
{applyResult.error}
> ) : ( <>Sync complete
{applyResult.updated} row{applyResult.updated !== 1 ? 's' : ''} updated
{applyResult.errors && applyResult.errors.length > 0 && ({applyResult.errors.length} error{applyResult.errors.length !== 1 ? 's' : ''} occurred
)} > )}