From 04cdd47521f51a60e81d214ed84dd9fc322b4f9b Mon Sep 17 00:00:00 2001 From: Lopinosaurus Date: Fri, 13 Mar 2026 21:50:06 +0100 Subject: [PATCH] rss: server-sided rather than client-sided --- public/app.js | 30 ++------------- server.js | 100 ++++++++++++++++++++++++++++++++++---------------- 2 files changed, 73 insertions(+), 57 deletions(-) diff --git a/public/app.js b/public/app.js index 33ce69f..2c2bf20 100644 --- a/public/app.js +++ b/public/app.js @@ -28,6 +28,10 @@ function connectWS() { } else if (msg.type === 'rootme_flag') { renderRootme(rootmeCache); showNotif(`FLAG ! ${msg.login} +${msg.gained} PTS — TOTAL : ${msg.newScore} PTS`); + } else if (msg.type === 'geo_news') { + showGeoBanner(msg.title, msg.link); + } else if (msg.type === 'anssi_news') { + showNotif(`Nouveau bulletin ANSSI : ${msg.title}`); } }); @@ -203,7 +207,6 @@ function updateClock() { const anssiList = document.getElementById('anssi-list'); const anssiStatus = document.getElementById('anssi-status'); -let seenAnssiLinks = null; async function loadAnssi() { anssiStatus.textContent = '...'; @@ -219,17 +222,6 @@ async function loadAnssi() { return; } - const currentAnssiLinks = new Set(items.map(i => i.link)); - if (seenAnssiLinks === null) { - seenAnssiLinks = currentAnssiLinks; - } else { - const newItems = items.filter(i => !seenAnssiLinks.has(i.link)); - if (newItems.length) { - showNotif(`Nouveau bulletin ANSSI : ${newItems[0].title}`); - seenAnssiLinks = currentAnssiLinks; - } - } - items.forEach(item => { const li = document.createElement('li'); li.className = 'anssi-item'; @@ -258,7 +250,6 @@ async function loadAnssi() { const geoList = document.getElementById('geo-list'); const geoStatus = document.getElementById('geo-status'); -let seenGeoLinks = null; async function loadGeo() { geoStatus.textContent = '...'; @@ -274,19 +265,6 @@ async function loadGeo() { return; } - const currentLinks = new Set(items.map(i => i.link)); - - if (seenGeoLinks === null) { - // Premier chargement : on mémorise sans jouer - seenGeoLinks = currentLinks; - } else { - const newGeoItems = items.filter(i => !seenGeoLinks.has(i.link)); - if (newGeoItems.length) { - showGeoBanner(newGeoItems[0].title, newGeoItems[0].link); - seenGeoLinks = currentLinks; - } - } - items.forEach(item => { const li = document.createElement('li'); li.className = 'anssi-item'; diff --git a/server.js b/server.js index e8561b5..715dc10 100644 --- a/server.js +++ b/server.js @@ -88,61 +88,95 @@ app.delete('/api/alert', (req, res) => { res.json({ ok: true }); }); -// ANSSI / CERT-FR RSS feed -app.get('/api/feeds/anssi', async (req, res) => { +// ── Feed pollers (détection server-side, broadcast WS) ────────────────────── + +const FEED_POLL_MS = 5 * 60 * 1000; + +// ANSSI +let anssiCache = null; +let seenAnssiLinks = null; + +async function pollAnssi() { try { const response = await fetch('https://www.cert.ssi.gouv.fr/feed/', { - headers: { 'User-Agent': 'CyberDashboard/1.0' }, - timeout: 10000 + headers: { 'User-Agent': 'CyberDashboard/1.0' }, timeout: 10000 }); const xml = await response.text(); const parser = new XMLParser({ ignoreAttributes: false }); - const parsed = parser.parse(xml); - const items = parsed?.rss?.channel?.item || []; + const items = parser.parse(xml)?.rss?.channel?.item || []; const entries = (Array.isArray(items) ? items : [items]) - .map(item => ({ - title: item.title || '', - link: item.link || '', - pubDate: item.pubDate || '', - description: item.description || '' - })) + .map(item => ({ title: item.title || '', link: item.link || '', pubDate: item.pubDate || '', description: item.description || '' })) .sort((a, b) => new Date(b.pubDate) - new Date(a.pubDate)) .slice(0, 7); - res.json(entries); + + anssiCache = entries; + const currentLinks = new Set(entries.map(i => i.link)); + if (seenAnssiLinks === null) { + seenAnssiLinks = currentLinks; + } else { + const newItems = entries.filter(i => !seenAnssiLinks.has(i.link)); + if (newItems.length) { + broadcast({ type: 'anssi_news', title: newItems[0].title, link: newItems[0].link }); + seenAnssiLinks = currentLinks; + } + } } catch (err) { - res.status(502).json({ error: 'Feed fetch failed', detail: err.message }); + console.error('[anssi] poll error:', err.message); } +} + +app.get('/api/feeds/anssi', async (req, res) => { + if (anssiCache) return res.json(anssiCache); + // Premier appel avant le premier poll + await pollAnssi(); + res.json(anssiCache || []); }); -// Géopolitique — Google News RSS (conflits, cyberattaques, Ukraine, Iran…) -app.get('/api/feeds/geo', async (req, res) => { - const query = encodeURIComponent( +// Géopolitique — Google News RSS +let geoCache = null; +let seenGeoLinks = null; + +const GEO_QUERY_URL = (() => { + const q = encodeURIComponent( 'Ukraine OR Russie OR Iran OR "Moyen-Orient" OR OTAN OR guerre OR conflit' + ' OR cyberattaque OR ransomware OR APT OR "zero-day" OR vulnérabilité OR hack OR malware OR breach' ); - const url = `https://news.google.com/rss/search?q=${query}&hl=fr&gl=FR&ceid=FR:fr`; + return `https://news.google.com/rss/search?q=${q}&hl=fr&gl=FR&ceid=FR:fr`; +})(); + +async function pollGeo() { try { - const response = await fetch(url, { - headers: { 'User-Agent': 'CyberDashboard/1.0' }, - timeout: 10000 + const response = await fetch(GEO_QUERY_URL, { + headers: { 'User-Agent': 'CyberDashboard/1.0' }, timeout: 10000 }); const xml = await response.text(); const parser = new XMLParser({ ignoreAttributes: false }); - const parsed = parser.parse(xml); - const items = parsed?.rss?.channel?.item || []; + const items = parser.parse(xml)?.rss?.channel?.item || []; const entries = (Array.isArray(items) ? items : [items]) - .map(item => ({ - title: item.title || '', - link: item.link || '', - pubDate: item.pubDate || '', - source: item.source?.['#text'] || item.source || '' - })) + .map(item => ({ title: item.title || '', link: item.link || '', pubDate: item.pubDate || '', source: item.source?.['#text'] || item.source || '' })) .sort((a, b) => new Date(b.pubDate) - new Date(a.pubDate)) .slice(0, 7); - res.json(entries); + + geoCache = entries; + const currentLinks = new Set(entries.map(i => i.link)); + if (seenGeoLinks === null) { + seenGeoLinks = currentLinks; + } else { + const newItems = entries.filter(i => !seenGeoLinks.has(i.link)); + if (newItems.length) { + broadcast({ type: 'geo_news', title: newItems[0].title, link: newItems[0].link }); + seenGeoLinks = currentLinks; + } + } } catch (err) { - res.status(502).json({ error: 'Geo feed fetch failed', detail: err.message }); + console.error('[geo] poll error:', err.message); } +} + +app.get('/api/feeds/geo', async (req, res) => { + if (geoCache) return res.json(geoCache); + await pollGeo(); + res.json(geoCache || []); }); // ── ICS / Calendar ────────────────────────────────────────────────────────── @@ -301,5 +335,9 @@ app.get('/api/rootme', (req, res) => { server.listen(PORT, () => { console.log(`Cyber Dashboard running on http://localhost:${PORT}`); + pollAnssi(); + setInterval(pollAnssi, FEED_POLL_MS); + pollGeo(); + setInterval(pollGeo, FEED_POLL_MS); startRootmePoller(); });