'use strict'; // ── WebSocket ──────────────────────────────────────────────────────────────── let ws; let wsReconnectTimer; function connectWS() { const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; ws = new WebSocket(`${proto}//${location.host}`); ws.addEventListener('open', () => { setWsStatus('CONNECTÉ', 'ok'); updateInfoWS('CONNECTÉ'); if (wsReconnectTimer) { clearTimeout(wsReconnectTimer); wsReconnectTimer = null; } }); ws.addEventListener('message', evt => { let msg; try { msg = JSON.parse(evt.data); } catch { return; } if (msg.type === 'alert') { showAlert(msg.message, msg.html, msg.image); } else if (msg.type === 'dismiss') { hideAlert(); } }); ws.addEventListener('close', () => { setWsStatus('DÉCONNECTÉ', 'err'); updateInfoWS('DÉCONNECTÉ'); wsReconnectTimer = setTimeout(connectWS, 3000); }); ws.addEventListener('error', () => { ws.close(); }); } function setWsStatus(text, cls) { const el = document.getElementById('ws-status'); el.textContent = text; el.className = 'widget-status ' + (cls || ''); } function updateInfoWS(text) { document.getElementById('info-ws').textContent = text; } // ── Alert overlay ──────────────────────────────────────────────────────────── const overlay = document.getElementById('alert-overlay'); const alertMessageEl = document.getElementById('alert-message'); const alertHtmlEl = document.getElementById('alert-html'); const alertIconEl = document.getElementById('alert-icon'); const alertImageEl = document.getElementById('alert-image'); const infoAlert = document.getElementById('info-alert'); // ── Alarm sound ─────────────────────────────────────────────────────────────── const alarmAudio = new Audio('/alert.mp3'); function playAlertSound() { alarmAudio.currentTime = 0; alarmAudio.play().catch(err => console.error('Audio play failed:', err)); } function stopAlertSound() { alarmAudio.pause(); alarmAudio.currentTime = 0; } // ── Alert overlay ───────────────────────────────────────────────────────────── let autoDismissTimer = null; function showAlert(message, html, image) { // Image if (image && image.trim()) { alertImageEl.innerHTML = ``; alertIconEl.style.display = 'none'; } else { alertImageEl.innerHTML = ''; alertIconEl.style.display = ''; } // Text / HTML alertMessageEl.textContent = message || ''; alertMessageEl.style.display = (message && !image) ? '' : (message ? '' : 'none'); alertHtmlEl.innerHTML = (html && html.trim()) ? html : ''; // Show overlay overlay.classList.remove('hidden'); overlay.style.animation = 'none'; void overlay.offsetWidth; overlay.style.animation = ''; infoAlert.textContent = 'ACTIVE'; infoAlert.style.color = 'var(--red)'; playAlertSound(); if (autoDismissTimer) clearTimeout(autoDismissTimer); autoDismissTimer = setTimeout(hideAlert, 60_000); } function hideAlert() { if (autoDismissTimer) { clearTimeout(autoDismissTimer); autoDismissTimer = null; } overlay.classList.add('hidden'); stopAlertSound(); infoAlert.textContent = 'INACTIVE'; infoAlert.style.color = ''; } // ── Clock ──────────────────────────────────────────────────────────────────── const clockTime = document.getElementById('clock-time'); const clockDate = document.getElementById('clock-date'); const headerClock = document.getElementById('header-clock'); const infoHost = document.getElementById('info-host'); const DAYS = ['Dimanche','Lundi','Mardi','Mercredi','Jeudi','Vendredi','Samedi']; const MONTHS = ['Jan','Fév','Mar','Avr','Mai','Jun','Jul','Aoû','Sep','Oct','Nov','Déc']; function pad(n) { return String(n).padStart(2, '0'); } function updateClock() { const now = new Date(); const h = pad(now.getHours()); const m = pad(now.getMinutes()); const s = pad(now.getSeconds()); const timeStr = `${h}:${m}:${s}`; const dateStr = `${DAYS[now.getDay()]} ${pad(now.getDate())} ${MONTHS[now.getMonth()]} ${now.getFullYear()}`; clockTime.textContent = timeStr; clockDate.textContent = dateStr; headerClock.textContent = `${dateStr} ${timeStr}`; } // ── ANSSI / CERT-FR feed ───────────────────────────────────────────────────── const anssiList = document.getElementById('anssi-list'); const anssiStatus = document.getElementById('anssi-status'); async function loadAnssi() { anssiStatus.textContent = '...'; anssiStatus.className = 'widget-status'; try { const resp = await fetch('/api/feeds/anssi'); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const items = await resp.json(); anssiList.innerHTML = ''; if (!items.length) { anssiList.innerHTML = '
  • Aucun bulletin
  • '; return; } items.forEach(item => { const li = document.createElement('li'); li.className = 'anssi-item'; const date = item.pubDate ? new Date(item.pubDate).toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' }) : ''; li.innerHTML = ` ${escapeHtml(item.title)} ${date ? `${escapeHtml(date)}` : ''} `; anssiList.appendChild(li); }); anssiStatus.textContent = `${items.length} bulletins`; anssiStatus.className = 'widget-status ok'; } catch (err) { anssiStatus.textContent = 'ERREUR'; anssiStatus.className = 'widget-status err'; anssiList.innerHTML = `
  • Erreur : ${escapeHtml(err.message)}
  • `; } } // ── CVE feed ────────────────────────────────────────────────────────────────── const cveList = document.getElementById('cve-list'); const cveStatus = document.getElementById('cve-status'); function severityClass(cvss) { if (cvss === undefined || cvss === null) return 'unknown'; const score = parseFloat(cvss); if (isNaN(score)) return 'unknown'; if (score >= 9.0) return 'critical'; if (score >= 7.0) return 'high'; if (score >= 4.0) return 'medium'; return 'low'; } function severityLabel(cvss) { const cls = severityClass(cvss); const map = { critical: 'CRITIQUE', high: 'ÉLEVÉ', medium: 'MOYEN', low: 'FAIBLE', unknown: '???' }; return map[cls]; } async function loadCve() { cveStatus.textContent = '...'; cveStatus.className = 'widget-status'; try { const resp = await fetch('/api/feeds/cve'); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const items = await resp.json(); cveList.innerHTML = ''; if (!items.length) { cveList.innerHTML = '
  • Aucune CVE
  • '; return; } items.forEach(item => { const cvss = item.cvss ?? item['cvss-score'] ?? null; const cls = severityClass(cvss); const label = severityLabel(cvss); const desc = item.summary || item.description || ''; const cveId = item.id || item['cve-id'] || 'CVE-????-????'; const li = document.createElement('li'); li.className = 'cve-item'; li.innerHTML = ` ${escapeHtml(label)}
    ${escapeHtml(cveId)}
    ${escapeHtml(desc)}
    `; cveList.appendChild(li); }); cveStatus.textContent = `${items.length} CVEs`; cveStatus.className = 'widget-status ok'; } catch (err) { cveStatus.textContent = 'ERREUR'; cveStatus.className = 'widget-status err'; cveList.innerHTML = `
  • Erreur : ${escapeHtml(err.message)}
  • `; } } // ── Init ───────────────────────────────────────────────────────────────────── function init() { infoHost.textContent = location.host; connectWS(); updateClock(); setInterval(updateClock, 1000); loadAnssi(); loadCve(); setInterval(loadAnssi, 5 * 60 * 1000); setInterval(loadCve, 5 * 60 * 1000); } // ── Helpers ─────────────────────────────────────────────────────────────────── function escapeHtml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function escapeAttr(str) { return String(str).replace(/"/g, '"').replace(/'/g, '''); } document.addEventListener('DOMContentLoaded', init);