rss: server-sided rather than client-sided
This commit is contained in:
parent
86838b0fb8
commit
04cdd47521
|
|
@ -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';
|
||||
|
|
|
|||
100
server.js
100
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();
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue