dashboard/server.js

128 lines
3.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 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')));
// 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 || ''
}));
res.json(entries);
} catch (err) {
res.status(502).json({ error: 'Feed fetch failed', detail: err.message });
}
});
// Last 10 CVEs from CIRCL
app.get('/api/feeds/cve', async (req, res) => {
try {
const response = await fetch('https://cve.circl.lu/api/last/10', {
headers: { 'User-Agent': 'CyberDashboard/1.0' },
timeout: 10000
});
const data = await response.json();
res.json(data);
} catch (err) {
res.status(502).json({ error: 'CVE fetch failed', detail: err.message });
}
});
server.listen(PORT, () => {
console.log(`Cyber Dashboard running on http://localhost:${PORT}`);
});