Fix: CVE Fetch via Two-Step Pagination statt URL-Filter

NVD API unterstützt keine kombinierten Filter (Timeout bei cvssV3Severity+pubStartDate).
Lösung: Erst totalResults holen, dann die letzten 60 Einträge mit startIndex abrufen
und lokal nach Score >= 5 / High+Critical filtern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 09:31:53 +02:00
parent c276b23293
commit 0b2943087d

View File

@@ -4,48 +4,68 @@ const https = require('https');
// In-memory CVE cache — refresh every hour // In-memory CVE cache — refresh every hour
let cveCache = { ts: 0, data: [] }; let cveCache = { ts: 0, data: [] };
function fetchCVEs() { function nvdGet(path) {
return new Promise((resolve) => { return new Promise((resolve, reject) => {
const opts = { const req = https.get({
hostname: 'services.nvd.nist.gov', hostname: 'services.nvd.nist.gov',
path: '/rest/json/cves/2.0?resultsPerPage=8&sortBy=published&sortOrder=desc', path,
headers: { 'User-Agent': 'IT-Nexus-TV/1.0' }, headers: { 'User-Agent': 'IT-Nexus-TV/1.0' },
timeout: 8000, }, (res) => {
};
const req = https.get(opts, (res) => {
let raw = ''; let raw = '';
res.on('data', c => raw += c); res.on('data', c => raw += c);
res.on('end', () => { res.on('end', () => { try { resolve(JSON.parse(raw)); } catch (e) { reject(e); } });
try { });
const json = JSON.parse(raw); req.on('error', reject);
const items = (json.vulnerabilities || []).map(v => { setTimeout(() => { req.destroy(); reject(new Error('timeout')); }, 15000);
});
}
function mapCVE(v) {
const cve = v.cve; const cve = v.cve;
const m31 = cve.metrics?.cvssMetricV31 || []; const m31 = cve.metrics?.cvssMetricV31 || [];
const m30 = cve.metrics?.cvssMetricV30 || []; const m30 = cve.metrics?.cvssMetricV30 || [];
const m = m31[0] || m30[0]; const m2 = cve.metrics?.cvssMetricV2 || [];
const m = m31[0] || m30[0] || m2[0];
const score = m?.cvssData?.baseScore || 0; const score = m?.cvssData?.baseScore || 0;
const sevRaw = (m?.cvssData?.baseSeverity || 'medium').toLowerCase(); let sevRaw = (m?.cvssData?.baseSeverity || m?.baseSeverity || 'medium').toLowerCase();
const sev = ['critical','high','medium','low'].includes(sevRaw) ? sevRaw : 'medium'; if (!['critical','high','medium','low'].includes(sevRaw)) {
sevRaw = score >= 9 ? 'critical' : score >= 7 ? 'high' : score >= 4 ? 'medium' : 'low';
}
const desc = cve.descriptions?.find(d => d.lang === 'en')?.value || ''; const desc = cve.descriptions?.find(d => d.lang === 'en')?.value || '';
const firstSentence = desc.split(/\.\s/)[0].replace(/\s+/g, ' ').trim(); const sentence = desc.split(/\.\s/)[0].replace(/\s+/g, ' ').trim();
const title = firstSentence.length > 110 ? firstSentence.substring(0, 110) + '…' : firstSentence || cve.id; const title = sentence.length > 110 ? sentence.substring(0, 110) + '…' : sentence || cve.id;
const pub = new Date(cve.published); const pub = new Date(cve.published);
const diffH = Math.round((Date.now() - pub.getTime()) / 3600000); const diffH = Math.round((Date.now() - pub.getTime()) / 3600000);
const published = diffH < 1 ? '< 1 Std.' : diffH < 24 ? `${diffH} Std.` : `${Math.round(diffH / 24)} Tag${Math.round(diffH / 24) !== 1 ? 'e' : ''}`; const published = diffH < 1 ? '< 1 Std.' : diffH < 24 ? `${diffH} Std.` : `${Math.round(diffH / 24)} Tag${Math.round(diffH / 24) !== 1 ? 'e' : ''}`;
// Vendor from CPE or references
let vendor = 'NVD'; let vendor = 'NVD';
const cpe = cve.configurations?.[0]?.nodes?.[0]?.cpeMatch?.[0]?.criteria || ''; const cpe = cve.configurations?.[0]?.nodes?.[0]?.cpeMatch?.[0]?.criteria || '';
const m2 = cpe.match(/cpe:2\.3:[ao]:([^:]+):/); const cm = cpe.match(/cpe:2\.3:[ao]:([^:]+):/);
if (m2) vendor = m2[1].charAt(0).toUpperCase() + m2[1].slice(1).replace(/_/g,' '); if (cm) vendor = cm[1].charAt(0).toUpperCase() + cm[1].slice(1).replace(/_/g, ' ');
return { id: cve.id, sev, cvss: score, title, vendor, published }; return { id: cve.id, sev: sevRaw, cvss: score, title, vendor, published };
}).filter(c => c.cvss >= 5).slice(0, 5); }
resolve(items.length >= 3 ? items : null);
} catch { resolve(null); } async function fetchCVEs() {
}); try {
}); // Step 1: get total count (fast, no filters)
req.on('error', () => resolve(null)); const r1 = await nvdGet('/rest/json/cves/2.0?resultsPerPage=1');
req.on('timeout', () => { req.destroy(); resolve(null); }); const total = r1.totalResults || 0;
}); if (total < 5) return null;
// Step 2: fetch the last ~60 entries (newest published CVEs)
const startIndex = Math.max(0, total - 60);
const r2 = await nvdGet(`/rest/json/cves/2.0?resultsPerPage=60&startIndex=${startIndex}`);
const vulns = (r2.vulnerabilities || []).reverse(); // newest first
const mapped = vulns.map(mapCVE).filter(c => c.cvss >= 5);
// Prefer high/critical, then fill with medium
const high = mapped.filter(c => ['critical','high'].includes(c.sev));
const medium = mapped.filter(c => c.sev === 'medium');
const merged = [...high, ...medium].slice(0, 5);
return merged.length >= 3 ? merged : null;
} catch {
return null;
}
} }
async function getStats(req, res) { async function getStats(req, res) {