rss: server-sided rather than client-sided

This commit is contained in:
Lopinosaurus 2026-03-13 21:50:06 +01:00
parent 86838b0fb8
commit 04cdd47521
2 changed files with 73 additions and 57 deletions

View File

@ -28,6 +28,10 @@ function connectWS() {
} else if (msg.type === 'rootme_flag') { } else if (msg.type === 'rootme_flag') {
renderRootme(rootmeCache); renderRootme(rootmeCache);
showNotif(`FLAG ! ${msg.login} +${msg.gained} PTS — TOTAL : ${msg.newScore} PTS`); 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 anssiList = document.getElementById('anssi-list');
const anssiStatus = document.getElementById('anssi-status'); const anssiStatus = document.getElementById('anssi-status');
let seenAnssiLinks = null;
async function loadAnssi() { async function loadAnssi() {
anssiStatus.textContent = '...'; anssiStatus.textContent = '...';
@ -219,17 +222,6 @@ async function loadAnssi() {
return; 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 => { items.forEach(item => {
const li = document.createElement('li'); const li = document.createElement('li');
li.className = 'anssi-item'; li.className = 'anssi-item';
@ -258,7 +250,6 @@ async function loadAnssi() {
const geoList = document.getElementById('geo-list'); const geoList = document.getElementById('geo-list');
const geoStatus = document.getElementById('geo-status'); const geoStatus = document.getElementById('geo-status');
let seenGeoLinks = null;
async function loadGeo() { async function loadGeo() {
geoStatus.textContent = '...'; geoStatus.textContent = '...';
@ -274,19 +265,6 @@ async function loadGeo() {
return; 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 => { items.forEach(item => {
const li = document.createElement('li'); const li = document.createElement('li');
li.className = 'anssi-item'; li.className = 'anssi-item';

100
server.js
View File

@ -88,61 +88,95 @@ app.delete('/api/alert', (req, res) => {
res.json({ ok: true }); res.json({ ok: true });
}); });
// ANSSI / CERT-FR RSS feed // ── Feed pollers (détection server-side, broadcast WS) ──────────────────────
app.get('/api/feeds/anssi', async (req, res) => {
const FEED_POLL_MS = 5 * 60 * 1000;
// ANSSI
let anssiCache = null;
let seenAnssiLinks = null;
async function pollAnssi() {
try { try {
const response = await fetch('https://www.cert.ssi.gouv.fr/feed/', { const response = await fetch('https://www.cert.ssi.gouv.fr/feed/', {
headers: { 'User-Agent': 'CyberDashboard/1.0' }, headers: { 'User-Agent': 'CyberDashboard/1.0' }, timeout: 10000
timeout: 10000
}); });
const xml = await response.text(); const xml = await response.text();
const parser = new XMLParser({ ignoreAttributes: false }); const parser = new XMLParser({ ignoreAttributes: false });
const parsed = parser.parse(xml); const items = parser.parse(xml)?.rss?.channel?.item || [];
const items = parsed?.rss?.channel?.item || [];
const entries = (Array.isArray(items) ? items : [items]) const entries = (Array.isArray(items) ? items : [items])
.map(item => ({ .map(item => ({ title: item.title || '', link: item.link || '', pubDate: item.pubDate || '', description: item.description || '' }))
title: item.title || '',
link: item.link || '',
pubDate: item.pubDate || '',
description: item.description || ''
}))
.sort((a, b) => new Date(b.pubDate) - new Date(a.pubDate)) .sort((a, b) => new Date(b.pubDate) - new Date(a.pubDate))
.slice(0, 7); .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) { } 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…) // Géopolitique — Google News RSS
app.get('/api/feeds/geo', async (req, res) => { let geoCache = null;
const query = encodeURIComponent( let seenGeoLinks = null;
const GEO_QUERY_URL = (() => {
const q = encodeURIComponent(
'Ukraine OR Russie OR Iran OR "Moyen-Orient" OR OTAN OR guerre OR conflit' + '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' ' 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 { try {
const response = await fetch(url, { const response = await fetch(GEO_QUERY_URL, {
headers: { 'User-Agent': 'CyberDashboard/1.0' }, headers: { 'User-Agent': 'CyberDashboard/1.0' }, timeout: 10000
timeout: 10000
}); });
const xml = await response.text(); const xml = await response.text();
const parser = new XMLParser({ ignoreAttributes: false }); const parser = new XMLParser({ ignoreAttributes: false });
const parsed = parser.parse(xml); const items = parser.parse(xml)?.rss?.channel?.item || [];
const items = parsed?.rss?.channel?.item || [];
const entries = (Array.isArray(items) ? items : [items]) const entries = (Array.isArray(items) ? items : [items])
.map(item => ({ .map(item => ({ title: item.title || '', link: item.link || '', pubDate: item.pubDate || '', source: item.source?.['#text'] || item.source || '' }))
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)) .sort((a, b) => new Date(b.pubDate) - new Date(a.pubDate))
.slice(0, 7); .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) { } 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 ────────────────────────────────────────────────────────── // ── ICS / Calendar ──────────────────────────────────────────────────────────
@ -301,5 +335,9 @@ app.get('/api/rootme', (req, res) => {
server.listen(PORT, () => { server.listen(PORT, () => {
console.log(`Cyber Dashboard running on http://localhost:${PORT}`); console.log(`Cyber Dashboard running on http://localhost:${PORT}`);
pollAnssi();
setInterval(pollAnssi, FEED_POLL_MS);
pollGeo();
setInterval(pollGeo, FEED_POLL_MS);
startRootmePoller(); startRootmePoller();
}); });