233 lines
7.8 KiB
JavaScript
233 lines
7.8 KiB
JavaScript
'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);
|
|
});
|