PageSourceSearch

https://www.techzick.com/threat-feed.js

js techzick.com collected 2026-10-02 01:45:29 UTC 19,110 bytes, 433 lines download raw bytes

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,'&amp;').replace(/</g,'&lt;')
187      .replace(/>/g,'&gt;').replace(/"/g,'&quot;');
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.