'use strict'; const express = require('express'); const http = require('http'); const WebSocket = require('ws'); const fetch = require('node-fetch'); const { XMLParser } = require('fast-xml-parser'); const path = require('path'); const fs = require('fs'); const PORT = process.env.DASHBOARD_PORT || 3000; const app = express(); app.use(express.json({ limit: '10mb' })); app.use(express.static(path.join(__dirname, 'public'))); const server = http.createServer(app); const wss = new WebSocket.Server({ server }); // Alert state let alertState = { active: false, message: '', html: '', image: '' }; // Broadcast to all connected WS clients function broadcast(data) { const payload = JSON.stringify(data); wss.clients.forEach(client => { if (client.readyState === WebSocket.OPEN) { client.send(payload); } }); } // Send current state to newly connected client wss.on('connection', ws => { if (alertState.active) { ws.send(JSON.stringify({ type: 'alert', message: alertState.message, html: alertState.html, image: alertState.image })); } }); // ── Routes ────────────────────────────────────────────────────────────────── app.get('/alert.mp3', (_req, res) => res.sendFile(path.resolve('alert.mp3'))); app.get('/news.mp3', (_req, res) => res.sendFile(path.resolve('news.mp3'))); app.get('/soft_alarm.mp3', (_req, res) => res.sendFile(path.resolve('soft_alarm.mp3'))); // Proxy: strip X-Frame-Options and CSP, forward any URL app.get('/proxy', async (req, res) => { const { url } = req.query; if (!url) return res.status(400).json({ error: 'Missing url parameter' }); try { const upstream = await fetch(url, { headers: { 'User-Agent': 'CyberDashboard/1.0' }, timeout: 10000 }); const contentType = upstream.headers.get('content-type') || 'text/html'; res.set('Content-Type', contentType); res.removeHeader('X-Frame-Options'); res.removeHeader('Content-Security-Policy'); upstream.body.pipe(res); } catch (err) { res.status(502).json({ error: 'Proxy fetch failed', detail: err.message }); } }); // Alert: GET state app.get('/api/alert', (req, res) => { res.json(alertState); }); // Alert: POST (trigger) app.post('/api/alert', (req, res) => { const { message = '', html = '', image = '' } = req.body; alertState = { active: true, message, html, image }; broadcast({ type: 'alert', message, html, image }); res.json({ ok: true }); }); // Alert: DELETE (dismiss) app.delete('/api/alert', (req, res) => { alertState = { active: false, message: '', html: '', image: '' }; broadcast({ type: 'dismiss' }); res.json({ ok: true }); }); // ANSSI / CERT-FR RSS feed app.get('/api/feeds/anssi', async (req, res) => { try { const response = await fetch('https://www.cert.ssi.gouv.fr/feed/', { 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 entries = (Array.isArray(items) ? items : [items]) .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); } catch (err) { res.status(502).json({ error: 'Feed fetch failed', detail: err.message }); } }); // Géopolitique — Google News RSS (conflits, cyberattaques, Ukraine, Iran…) app.get('/api/feeds/geo', async (req, res) => { const query = 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`; try { const response = await fetch(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 entries = (Array.isArray(items) ? items : [items]) .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); } catch (err) { res.status(502).json({ error: 'Geo feed fetch failed', detail: err.message }); } }); // Root-me ranking const ROOTME_POLL_MS = 10 * 60 * 1000; let rootmeCache = null; let rootmePrevScores = {}; // login → last known score const ROOTME_REQUEST_DELAY_MS = 500; const rootmePlayerCache = {}; // id → { login, score, rank } const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); async function fetchRootmeRanking(apiKey) { const raw = fs.readFileSync(path.resolve('logins.txt'), 'utf8'); const ids = raw.split('\n').map(l => l.trim()).filter(Boolean); const headers = { 'Cookie': `api_key=${apiKey}`, 'User-Agent': 'CyberDashboard/1.0' }; const results = []; for (const id of ids) { try { const resp = await fetch( `https://api.www.root-me.org/auteurs/${id}`, { headers, timeout: 10000 } ); if (resp.status === 429) { console.warn(`[rootme] rate-limited on id "${id}", using cached value`); if (rootmePlayerCache[id]) results.push(rootmePlayerCache[id]); } else { const profile = await resp.json(); const profileRaw = Array.isArray(profile) ? profile[0] : profile; const user = profileRaw?.['0'] ?? profileRaw; if (user && !user.error) { const entry = { login: user.nom || id, score: Number(user.score) || 0, rank: user.position || null }; rootmePlayerCache[id] = entry; results.push(entry); } } } catch (err) { console.error(`[rootme] fetch error for id "${id}":`, err.message); if (rootmePlayerCache[id]) results.push(rootmePlayerCache[id]); } await sleep(ROOTME_REQUEST_DELAY_MS); } return results.sort((a, b) => b.score - a.score); } async function pollRootme() { const apiKey = process.env.ROOTME_API_KEY; if (!apiKey) return; try { const ranking = await fetchRootmeRanking(apiKey); // Detect score gains and broadcast flag events const isFirstPoll = Object.keys(rootmePrevScores).length === 0; if (!isFirstPoll) { ranking.forEach(user => { const prev = rootmePrevScores[user.login]; if (prev !== undefined && user.score > prev) { const gained = user.score - prev; console.log(`[rootme] FLAG ! ${user.login} +${gained} pts (${prev} -> ${user.score})`); broadcast({ type: 'rootme_flag', login: user.login, gained, newScore: user.score }); } }); } ranking.forEach(u => { rootmePrevScores[u.login] = u.score; }); rootmeCache = ranking; broadcast({ type: 'rootme_update', ranking }); console.log(`[rootme] polled — ${ranking.length} joueur(s)`); } catch (err) { console.error('[rootme] poll error:', err.message); } } app.get('/api/rootme', (req, res) => { if (!process.env.ROOTME_API_KEY) return res.status(503).json({ error: 'ROOTME_API_KEY not configured' }); if (!rootmeCache) return res.json([]); res.json(rootmeCache); }); server.listen(PORT, () => { console.log(`Cyber Dashboard running on http://localhost:${PORT}`); pollRootme(); setInterval(pollRootme, ROOTME_POLL_MS); });