diff --git a/backend/src/controllers/tv.controller.js b/backend/src/controllers/tv.controller.js index ebe15ee..5b88a1a 100644 --- a/backend/src/controllers/tv.controller.js +++ b/backend/src/controllers/tv.controller.js @@ -4,50 +4,70 @@ const https = require('https'); // In-memory CVE cache — refresh every hour let cveCache = { ts: 0, data: [] }; -function fetchCVEs() { - return new Promise((resolve) => { - const opts = { +function nvdGet(path) { + return new Promise((resolve, reject) => { + const req = https.get({ 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' }, - timeout: 8000, - }; - const req = https.get(opts, (res) => { + }, (res) => { let raw = ''; res.on('data', c => raw += c); - res.on('end', () => { - try { - const json = JSON.parse(raw); - const items = (json.vulnerabilities || []).map(v => { - const cve = v.cve; - const m31 = cve.metrics?.cvssMetricV31 || []; - const m30 = cve.metrics?.cvssMetricV30 || []; - const m = m31[0] || m30[0]; - const score = m?.cvssData?.baseScore || 0; - const sevRaw = (m?.cvssData?.baseSeverity || 'medium').toLowerCase(); - const sev = ['critical','high','medium','low'].includes(sevRaw) ? sevRaw : 'medium'; - const desc = cve.descriptions?.find(d => d.lang === 'en')?.value || ''; - const firstSentence = desc.split(/\.\s/)[0].replace(/\s+/g, ' ').trim(); - const title = firstSentence.length > 110 ? firstSentence.substring(0, 110) + '…' : firstSentence || cve.id; - const pub = new Date(cve.published); - 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' : ''}`; - // Vendor from CPE or references - let vendor = 'NVD'; - const cpe = cve.configurations?.[0]?.nodes?.[0]?.cpeMatch?.[0]?.criteria || ''; - const m2 = cpe.match(/cpe:2\.3:[ao]:([^:]+):/); - if (m2) vendor = m2[1].charAt(0).toUpperCase() + m2[1].slice(1).replace(/_/g,' '); - return { id: cve.id, sev, cvss: score, title, vendor, published }; - }).filter(c => c.cvss >= 5).slice(0, 5); - resolve(items.length >= 3 ? items : null); - } catch { resolve(null); } - }); + res.on('end', () => { try { resolve(JSON.parse(raw)); } catch (e) { reject(e); } }); }); - req.on('error', () => resolve(null)); - req.on('timeout', () => { req.destroy(); resolve(null); }); + req.on('error', reject); + setTimeout(() => { req.destroy(); reject(new Error('timeout')); }, 15000); }); } +function mapCVE(v) { + const cve = v.cve; + const m31 = cve.metrics?.cvssMetricV31 || []; + const m30 = cve.metrics?.cvssMetricV30 || []; + const m2 = cve.metrics?.cvssMetricV2 || []; + const m = m31[0] || m30[0] || m2[0]; + const score = m?.cvssData?.baseScore || 0; + let sevRaw = (m?.cvssData?.baseSeverity || m?.baseSeverity || 'medium').toLowerCase(); + 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 sentence = desc.split(/\.\s/)[0].replace(/\s+/g, ' ').trim(); + const title = sentence.length > 110 ? sentence.substring(0, 110) + '…' : sentence || cve.id; + const pub = new Date(cve.published); + 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' : ''}`; + let vendor = 'NVD'; + const cpe = cve.configurations?.[0]?.nodes?.[0]?.cpeMatch?.[0]?.criteria || ''; + const cm = cpe.match(/cpe:2\.3:[ao]:([^:]+):/); + if (cm) vendor = cm[1].charAt(0).toUpperCase() + cm[1].slice(1).replace(/_/g, ' '); + return { id: cve.id, sev: sevRaw, cvss: score, title, vendor, published }; +} + +async function fetchCVEs() { + try { + // Step 1: get total count (fast, no filters) + const r1 = await nvdGet('/rest/json/cves/2.0?resultsPerPage=1'); + 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) { try { const db = getDatabase();