1/* ============================================================ 2 TECHZICK â AI Sentinel Live Threat Feed 3 Sources: CISA KEV (known exploited) + NVD CVE API (recent) 4 Merges with curated static threats, auto-renders blog feed. 5 No build step. No backend. Pure client-side. 6 ============================================================ */ 7 8const TechzickFeed = (() => { 9 10 // ââ Constants âââââââââââââââââââââââââââââââââââââââââââââ 11 const KEV_URL = 'https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json'; 12 const NVD_URL = 'https://services.nvd.nist.gov/rest/json/cves/2.0'; 13 14 const KEV_CACHE = 'tz_kev_v4'; 15 const NVD_CACHE = 'tz_nvd_v2'; 16 const CACHE_TTL = 15 * 60 * 1000; // 15 minutes 17 const SIX_MO_MS = 183 * 24 * 60 * 60 * 1000; 18 const ACTIVE_MS = 30 * 24 * 60 * 60 * 1000; 19 const NVD_DAYS = 3; // short window so all entries fit in one page 20 21 // ââ Generic cache helpers âââââââââââââââââââââââââââââââââ 22 function cacheRead(key) { 23 try { 24 const raw = localStorage.getItem(key); 25 if (!raw) return { fresh: null, stale: null }; 26 const obj = JSON.parse(raw); 27 const age = Date.now() - obj.ts; 28 return age < CACHE_TTL 29 ? { fresh: obj.data, stale: null } 30 : { fresh: null, stale: obj.data }; 31 } catch { return { fresh: null, stale: null }; } 32 } 33 34 function cacheWrite(key, data) { 35 try { localStorage.setItem(key, JSON.stringify({ ts: Date.now(), data })); } catch {} 36 } 37 38 // ââ CISA KEV fetch ââââââââââââââââââââââââââââââââââââââââ 39 async function kevDoFetch() { 40 const res = await fetch(KEV_URL); 41 if (!res.ok) throw new Error(`KEV HTTP ${res.status}`); 42 const json = await res.json(); 43 const cutoff = Date.now() - SIX_MO_MS; 44 const recent = json.vulnerabilities 45 .filter(v => new Date(v.dateAdded).getTime() >= cutoff) 46 .sort((a, b) => new Date(b.dateAdded) - new Date(a.dateAdded)); 47 cacheWrite(KEV_CACHE, recent); 48 return recent; 49 } 50 51 // Returns [data, isStale] â always serves something immediately 52 // and fires a background refresh when the cache is stale. 53 async function fetchKEV() { 54 const { fresh, stale } = cacheRead(KEV_CACHE); 55 if (fresh) return [fresh, false]; 56 if (stale) { 57 kevDoFetch().then(() => refreshBlog()).catch(() => {}); 58 return [stale, true]; 59 } 60 return [await kevDoFetch(), false]; 61 } 62 63 // ââ NVD CVE fetch âââââââââââââââââââââââââââââââââââââââââ 64 // Extract the highest CVSS base score from any metric version 65 function nvdScore(cve) { 66 const sources = [ 67 ...(cve.metrics?.cvssMetricV40 || []), 68 ...(cve.metrics?.cvssMetricV31 || []), 69 ...(cve.metrics?.cvssMetricV30 || []), 70 ...(cve.metrics?.cvssMetricV2 || []), 71 ]; 72 return sources.reduce((max, m) => Math.max(max, m.cvssData?.baseScore || 0), 0); 73 } 74 75 async function nvdDoFetch() { 76 const end = new Date(); 77 const start = new Date(Date.now() - NVD_DAYS * 24 * 60 * 60 * 1000); 78 const fmt = d => d.toISOString().replace(/\.\d{3}Z$/, '.000'); 79 // No severity filter â NVD's cvssV3Severity only matches fully-analyzed CVEs; 80 // newly submitted ("Received") entries are excluded even if scorer-rated CRITICAL. 81 // We filter locally by score >= 7.0 after extracting from any available source. 82 const url = `${NVD_URL}?pubStartDate=${fmt(start)}&pubEndDate=${fmt(end)}&resultsPerPage=100`; 83 const res = await fetch(url); 84 if (!res.ok) throw new Error(`NVD HTTP ${res.status}`); 85 const json = await res.json(); 86 const vulns = (json.vulnerabilities || []) 87 .filter(v => nvdScore(v.cve) >= 7.0) 88 .sort((a, b) => new Date(b.cve.published) - new Date(a.cve.published)) 89 .slice(0, 20); 90 cacheWrite(NVD_CACHE, vulns); 91 return vulns; 92 } 93 94 async function fetchNVD() { 95 const { fresh, stale }
95 = cacheRead(NVD_CACHE); 96 if (fresh) return [fresh, false]; 97 if (stale) { 98 nvdDoFetch().then(() => refreshBlog()).catch(() => {}); 99 return [stale, true]; 100 } 101 return [await nvdDoFetch(), false]; 102 } 103 104 // ââ KEV â unified threat ââââââââââââââââââââââââââââââââââ 105 function kevToThreat(kev) { 106 const isRansomware = kev.knownRansomwareCampaignUse === 'Known'; 107 const type = isRansomware ? 'Ransomware' : 'Zero-Day'; 108 const severity = isRansomware ? 'CRITICAL' : 'HIGH'; 109 const isActive = (Date.now() - new Date(kev.dateAdded).getTime()) < ACTIVE_MS; 110 const vuln = kev.vulnerabilityName.length > 64 111 ? kev.vulnerabilityName.slice(0, 62) + 'â¦' 112 : kev.vulnerabilityName; 113 return { 114 id: kev.cveID.toLowerCase(), 115 cveID: kev.cveID, 116 date: kev.dateAdded, 117 title: `${kev.cveID} â ${kev.vendorProject} ${kev.product}: ${vuln}`, 118 type, severity, 119 excerpt: kev.shortDescription, 120 tags: [kev.cveID, kev.vendorProject, kev.product.split(' ')[0]].filter(Boolean), 121 isActive, 122 source: 'CISA KEV', 123 sourceUrl: 'https://www.cisa.gov/known-exploited-vulnerabilities-catalog', 124 nvdUrl: `https://nvd.nist.gov/vuln/detail/${kev.cveID}`, 125 requiredAction: kev.requiredAction, 126 dueDate: kev.dueDate, 127 }; 128 } 129 130 // ââ NVD â unified threat ââââââââââââââââââââââââââââââââââ 131 function nvdToThreat(entry) { 132 const cve = entry.cve; 133 const id = cve.id; 134 const desc = (cve.descriptions || []).find(d => d.lang === 'en')?.value || 'No description available.'; 135 const pub = (cve.published || '').slice(0, 10) || new Date().toISOString().slice(0, 10); 136 137 const score = nvdScore(cve); 138 const severity = score >= 9.0 ? 'CRITICAL' : 'HIGH'; 139 140 const cpeStr = cve.configurations?.[0]?.nodes?.[0]?.cpeMatch?.[0]?.criteria || ''; 141 const cp = cpeStr.split(':'); 142 const vendor = cp[3] ? cp[3].replace(/_/g, ' ') : ''; 143 const product = cp[4] ? cp[4].replace(/_/g, ' ') : ''; 144 145 const titleSuffix = desc.length > 72 ? desc.slice(0, 70) + 'â¦' : desc; 146 const isActive = (Date.now() - new Date(pub).getTime()) < ACTIVE_MS; 147 148 return { 149 id: id.toLowerCase(), 150 cveID: id, 151 date: pub, 152 title: `${id}${vendor ? ' â ' + vendor + (product ? ' ' + product : '') + ': ' : ' â '}${titleSuffix}`, 153 type: 'Zero-Day', 154 severity, 155 excerpt: desc.length > 185 ? desc.slice(0, 183) + 'â¦' : desc,
156 tags: [id, vendor, product].filter(Boolean).slice(0, 4), 157 isActive, 158 source: 'NVD', 159 sourceUrl: `https://nvd.nist.gov/vuln/detail/${id}`, 160 nvdUrl: `https://nvd.nist.gov/vuln/detail/${id}`, 161 requiredAction: 'Apply vendor security patches immediately.', 162 dueDate: null, 163 }; 164 } 165 166 // ââ Helpers âââââââââââââââââââââââââââââââââââââââââââââââ 167 function typeColor(type) { 168 return { Ransomware: 'purple', 'Zero-Day': 'orange', 'AI/LLM Attack': 'cyan' }[type] || ''; 169 } 170 171 function typeBadgeClass(type) { 172 return { 173 'Ransomware': 'Ransomware', 174 'Zero-Day': 'Zero-Day', 175 'Nation-State': 'Nation-State', 176 'Data Breach': 'Data-Breach', 177 'AI/LLM Attack': 'AI-Attack', 178 'Supply Chain': 'Supply-Chain', 179 'Compliance': 'Compliance', 180 'Regulatory': 'Compliance', 181 }[type] || 'Zero-Day'; 182 } 183 184 function esc(s) { 185 return String(s) 186 .replace(/&/g,'&').replace(/</g,'<') 187 .replace(/>/g,'>').replace(/"/g,'"'); 188 } 189 190 // ââ Deduplicate by CVE ID âââââââââââââââââââââââââââââââââ 191 function dedup(threats) { 192 const seen = new Set(); 193 return threats.filter(t => { 194 const k = (t.cveID || t.id).toUpperCase(); 195 if (seen.has(k)) return false; 196 seen.add(k); return true; 197 }); 198 } 199 200 // ââ Merge live + static âââââââââââââââââââââââââââââââââââ 201 // Live entries (CISA KEV + NVD) always lead, sorted newest first. 202 // Curated static entries fill the remaining slots. 203 function mergeThreats(live, statics) { 204 const liveCVEs = new Set(live.map(t => (t.cveID || t.id).toUpperCase())); 205 const filtered = statics.filter(t => { 206 const m = t.title.match(/CVE-\d{4}-\d+/i); 207 return !m || !liveCVEs.has(m[0].toUpperCase()); 208 }); 209 const sortedLive = [...live].sort((a, b) => new Date(b.date) - new Date(a.date)); 210 const sortedStatic = [...filtered].sort((a, b) => new Date(b.date) - new Date(a.date)); 211 return [...sortedLive, ...sortedStatic]; 212 } 213 214 // ââ Blog card HTML ââââââââââââââââââââââââââââââââââââââââ 215 function blogCardHTML(t, isFeatured) { 216 const color = typeColor(t.type); 217 const catCls = color ? `blog-category blog-category--${color}` : 'blog-category'; 218 const tagCls = (isFeatured && color) ? `blog-tag blog-tag--${color}` : 'blog-tag'; 219 const linkCls = (isFeatured && color) ? `card-link card-link--${color}` : 'card-link'; 220 const dotCls = (isFeatured && color) ? `author-dot author-dot--${color}` : 'author-dot'; 221 const isLive = t.source === 'CISA KEV' || t.source === 'NVD'; 222 const excerpt = t.excerpt.length > 185 ? t.excerpt.slice(0, 183) + 'â¦' : t.excerpt; 223 const href = isLive ? esc(t.nvdUrl) : `threat-intel.html#${t.anchor || t.id}`; 224 const target = isLive ? 'target="_blank" rel="noopener"' : ''; 225 const label = isLive ? 'NVD Details' : 'Full Details'; 226 227 const activeBadge = t.isActive 228 ? `<div class="blog-breaking-badge"><span class="pulse-dot"></span><span class="mono">ACTIVE</span></div>` 229 : ''; 230 231 const sourceBadge = t.source === 'CISA KEV' 232 ? `<span class="feed-source-badge mono">CISA KEV</span>` 233 : t.source === 'NVD' 234 ? `<span class="feed-source-badge mono">NVD</span>` 235 : `<span class="mono">AI Sentinel</span>`; 236 237 return ` 238 <article class="blog-card${isFeatured ? ' blog-card--featured' : ''}"> 239 <div class="blog-card-top"> 240 <span class="${catCls}">${esc(t.type)}</span> 241 <time class="blog-date mono" datetime="${esc(t.date)}">${esc(t.date)}</time> 242 </div> 243 ${activeBadge} 244 <h3 class="blog-title">${esc(t.title)}</h3> 245 <p class="blog-excerpt">${esc(excerpt)}</p> 246 <div class="blog-tags"> 247 ${t.tags.slice(0, 4).map(tag => `<span class="${tagCls}">${esc(tag)}</span>`).join('')} 248 </div> 249 <div class="blog-footer"> 250 <div class="blog-author">
251 <span class="${dotCls}"></span> 252 ${sourceBadge} 253 </div> 254 <a href="${href}" class="${linkCls}" ${target}>${label} <span aria-hidden="true">â</span></a> 255 </div> 256 </article>`; 257 } 258 259 // ââ Tracker card HTML âââââââââââââââââââââââââââââââââââââ 260 function trackerCardHTML(t) { 261 const statusClass = t.isActive ? 'Active' : 'Patched'; 262 const statusText = t.isActive ? 'â Active' : 'â Patched'; 263 const action = t.requiredAction || ''; 264 const due = t.dueDate 265 ? ` <span style="color:var(--cyan);margin-left:6px;">Due: ${esc(t.dueDate)}</span>` 266 : ''; 267 return ` 268 <article class="attack-card" data-type="${esc(t.type)}" id="${esc(t.id)}"> 269 <div class="attack-meta"> 270 <span class="attack-date">${esc(t.date)}</span> 271 <span class="severity-badge ${esc(t.severity)}">${esc(t.severity)}</span> 272 <span class="type-badge ${typeBadgeClass(t.type)}">${esc(t.type)}</span> 273 <span class="attack-status ${statusClass}">${statusText}</span> 274 </div> 275 <div class="attack-body"> 276 <h2 class="attack-title">${esc(t.title)}</h2> 277 <p class="attack-summary">${esc(t.excerpt)}</p> 278 <div class="attack-tags"> 279 ${t.tags.map(tag => `<span class="attack-tag">${esc(tag)}</span>`).join('')} 280 </div> 281 ${action ? `<div class="attack-impact"><strong>Required Action:</strong> ${esc(action)}${due}</div>` : ''} 282 </div> 283 <div class="attack-actions"> 284 <a class="ti-source-link" href="${esc(t.sourceUrl)}" target="_blank" rel="noopener"> 285 <svg viewBox="0 0 12 12" fill="none"><path d="M2 10L10 2M10 2H6M10 2v4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg> 286 ${esc(t.source || 'CISA KEV')} 287 </a> 288 <a class="ti-source-link" href="${esc(t.nvdUrl)}" target="_blank" rel="noopener"> 289 <svg viewBox="0 0 12 12" fill="none"><path d="M2 10L10 2M10 2H6M10 2v4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg> 290 NVD Details 291 </a> 292 </div> 293 </article>`; 294 } 295 296 // ââ Loading skeletons âââââââââââââââââââââââââââââââââââââ 297 function blogSkeleton(n) { 298 return Array.from({ length: n }, () => ` 299 <article class="blog-card skel-card" aria-hidden="true"> 300 <div class="skel-line" style="width:30%"></div> 301 <div class="skel-line" style="width:85%;height:20px;margin-top:10px"></div> 302 <div class="skel-line" style="width:60%;height:14px;margin-top:6px"></div> 303 <div class="skel-line" style="width:100%;height:52px;margin-top:14px;border-radius:6px"></div> 304 </article>`).join(''); 305 } 306 307 function trackerSkeleton() { 308 return `<div class="skel-tracker-msg"> 309 <span class="pulse-dot"></span> 310 <span class="mono" style="font-size:.78rem;color:var(--text-dim)"> 311 Fetching live threat feeds⦠312 </span> 313 </div>`; 314 } 315 316 // ââ Reveal animation ââââââââââââââââââââââââââââââââââââââ 317 function animateIn(container) { 318 const obs = new IntersectionObserver((entries) => { 319 entries.forEach(e => { 320 if (e.isIntersecting) { e.target.classList.add('visible'); obs.unobserve(e.target); } 321 }); 322 }, { threshold: 0.08, rootMargin: '0px 0px -20px 0px' }); 323 container.querySelectorAll('.blog-card, .attack-card').forEach((el, i) => { 324 el.classList.add('reveal'); 325 const d = i % 4; 326 if (d) el.classList.add(`reveal-delay-${d}`); 327 obs.observe(el); 328 }); 329 } 330 331 // ââ BLOG RENDER âââââââââââââââââââââââââââââââââââââââââââ 332 async function renderBlog() { 333 const grid = document.getElementById('blog-grid'); 334 const countEl = document.getElementById('threat-count'); 335 if (!grid) return; 336 337 const statics = (typeof THREATS !== 'undefined') ? THREATS : []; 338 339 // Fetch CISA KEV and NVD in parallel; neither failing should block the other 340 const [kevRes, nvdRes] = await Promise.allSettled([fetchKEV(), fetchNVD()]); 341
342 const kevLive = kevRes.status === 'fulfilled' ? kevRes.value[0].map(kevToThreat) : []; 343 const nvdLive = nvdRes.status === 'fulfilled' ? nvdRes.value[0].map(nvdToThreat) : []; 344 345 if (kevRes.status === 'rejected') console.warn('[AI Sentinel] CISA KEV unavailable:', kevRes.reason); 346 if (nvdRes.status === 'rejected') console.warn('[AI Sentinel] NVD unavailable:', nvdRes.reason); 347 348 const allLive = dedup([...kevLive, ...nvdLive]); 349 const merged = mergeThreats(allLive, statics); 350 const top4 = merged.slice(0, 4); 351 352 if (top4.length === 0) { 353 grid.innerHTML = '<p style="color:var(--text-dim);text-align:center;padding:32px 0;">No recent threats available.</p>'; 354 return; 355 } 356 357 grid.innerHTML = top4.map((t, i) => blogCardHTML(t, i === 0)).join(''); 358 animateIn(grid); 359 360 if (countEl) countEl.textContent = merged.length; 361 } 362 363 // Called by background fetches when fresh data arrives 364 function refreshBlog() { 365 const grid = document.getElementById('blog-grid'); 366 if (grid && !grid.querySelector('.skel-card')) renderBlog(); 367 } 368 369 // ââ BLOG INIT ââââââââââââââââââââââââââââââââââââââââââââ 370 async function initBlog() { 371 const grid = document.getElementById('blog-grid'); 372 if (!grid) return; 373 grid.innerHTML = blogSkeleton(4); 374 await renderBlog(); 375 } 376 377 // ââ TRACKER INIT (threat-intel.html) âââââââââââââââââââââ 378 async function initTracker() { 379 const liveGrid = document.getElementById('live-kev-grid'); 380 const liveCount = document.getElementById('live-kev-count'); 381 const lastEl = document.getElementById('live-kev-updated'); 382 const lastFull = document.getElementById('live-kev-catalog-date'); 383 if (!liveGrid) return; 384 385 liveGrid.innerHTML = trackerSkeleton(); 386 387 try { 388 const [kev] = await fetchKEV(); 389 const threats = kev.map(kevToThreat); 390 391 liveGrid.innerHTML = threats.map(trackerCardHTML).join(''); 392 animateIn(liveGrid); 393 394 if (liveCount) liveCount.textContent = threats.length; 395 396 const { fresh, stale } = cacheRead(KEV_CACHE); 397 const cached = fresh || stale; 398 if (lastEl && cached) { 399 const mins = Math.round((Date.now() - (JSON.parse(localStorage.getItem(KEV_CACHE) || '{}').ts || Date.now())) / 60000); 400 lastEl.textContent = mins < 1 ? 'just now' : `${mins}m ago`; 401 } 402 if (lastFull && kev.length) lastFull.textContent = kev[0].dateAdded; 403 404 if (typeof computeTrackerStats === 'function') computeTrackerStats(); 405 406 } catch (err) { 407 liveGrid.innerHTML = ` 408 <div style="padding:20px 0;color:var(--text-dim);font-size:.875rem;"> 409 Live feed unavailable. 410 <a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog" 411 target="_blank" rel="noopener" style="color:var(--cyan);margin-left:6px;"> 412 View on CISA â 413 </a> 414 </div>`; 415 console.warn('[AI Sentinel] Tracker KEV fetch failed:', err); 416 } 417 } 418 419 // ââ Auto-init on DOM ready ââââââââââââââââââââââââââââââââ 420 function init() { initBlog(); initTracker(); } 421 422 if (document.readyState === 'loading') { 423 document.addEventListener('DOMContentLoaded', init); 424 } else { 425 init(); 426 } 427 428 // Auto-refresh every 15 minutes 429 setInterval(init, CACHE_TTL); 430 431 return { refresh: init }; 432 433})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.