From 59ae200324af25530556652cad13318b284d3217 Mon Sep 17 00:00:00 2001 From: Lopinosaurus Date: Wed, 11 Mar 2026 23:05:56 +0100 Subject: [PATCH] root-me: add logins --- README.md | 113 +++++++++++++++++++++++++++++++++++++++++++++++++++++ logins.txt | 9 ++++- server.js | 35 +++++++++++------ 3 files changed, 145 insertions(+), 12 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..9892301 --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ +# Cyber Dashboard + +Real-time cyber monitoring dashboard: ANSSI/CERT-FR feeds, geopolitical news, Kaspersky live map, and Root-me ranking. + +## Requirements + +- Node.js ≥ 18 +- npm + +## Installation + +```bash +npm install +``` + +## Configuration + +| Environment variable | Description | Required | +|---|---|---| +| `ROOTME_API_KEY` | Root-me API key (Profile → Preferences) | Yes (Root-me widget) | +| `DASHBOARD_PORT` | Listening port (default: `3000`) | No | + +## Config files + +**`logins.txt`** — one numeric Root-me user ID per line: +``` +546528 +123456 +``` + +The ID can be found in the Root-me profile URL: `root-me.org/Username?inc=score&id_auteur=XXXXXX` + +## Start + +```bash +ROOTME_API_KEY= node server.js +``` + +Then open `http://localhost:3000`. + +## Manual alerts + +```bash +# Trigger an alert +./alert.sh "INTRUSION DETECTED" + +# With custom HTML +./alert.sh "TITLE" path/to/content.html + +# Dismiss the alert +./dismiss.sh +``` + +## Architecture + +``` +dashboard/ +├── server.js # Express + WebSocket server +├── public/ +│ ├── index.html # 2×2 grid with carousel +│ ├── style.css # Dark cyber theme +│ └── app.js # WS client + feed polling +├── logins.txt # Root-me user IDs to track +├── alert.sh # Triggers an alert via POST +└── dismiss.sh # Dismisses the alert via DELETE +``` + +### Endpoints + +| Method | Route | Description | +|---|---|---| +| `GET` | `/api/feeds/anssi` | CERT-FR bulletins (RSS) | +| `GET` | `/api/feeds/geo` | Geopolitical news (Google News RSS) | +| `GET` | `/api/rootme` | Root-me ranking (server cache) | +| `GET` | `/api/alert` | Current alert state | +| `POST` | `/api/alert` | Trigger an alert `{ message, html, image }` | +| `DELETE` | `/api/alert` | Dismiss the alert | +| `GET` | `/proxy?url=` | HTTP proxy (strips X-Frame-Options/CSP) | + +### WebSocket events + +The server pushes the following events to all connected clients: + +| `type` | Payload | Description | +|---|---|---| +| `alert` | `{ message, html, image }` | Alert triggered | +| `dismiss` | — | Alert dismissed | +| `rootme_update` | `{ ranking[] }` | Updated Root-me ranking | +| `rootme_flag` | `{ login, gained, newScore }` | A player just flagged a challenge | + +## Contributing + +### Commit conventions + +``` +: +``` + +Examples: +``` +root-me: basic ranking +alerts: new alerts +all: init +``` + +The scope reflects the component changed (`root-me`, `alerts`, `feeds`, `ui`, `server`, `all` for cross-cutting changes). + +### Adding a widget + +1. Add a `
` in `index.html` +2. Assign its grid cell in `style.css` (`grid-column` / `grid-row`) +3. Add it to `CAROUSEL_PAGES` in `app.js` +4. Add the corresponding endpoint in `server.js` if needed diff --git a/logins.txt b/logins.txt index 217342c..1d2571d 100644 --- a/logins.txt +++ b/logins.txt @@ -1,3 +1,10 @@ 724433 546528 - +709915 +1078733 +1060488 +967844 +716070 +1071634 +785590 +963602 diff --git a/server.js b/server.js index 3a353ac..801ec5c 100644 --- a/server.js +++ b/server.js @@ -146,31 +146,44 @@ 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 = await Promise.all(ids.map(async id => { + 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) throw new Error('rate-limited'); - const profile = await resp.json(); - const profileRaw = Array.isArray(profile) ? profile[0] : profile; - const user = profileRaw?.['0'] ?? profileRaw; - if (!user || user.error) return null; - - return { login: user.nom || id, score: Number(user.score) || 0, rank: user.position || null }; + 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); - return null; + if (rootmePlayerCache[id]) results.push(rootmePlayerCache[id]); } - })); + await sleep(ROOTME_REQUEST_DELAY_MS); + } - return results.filter(Boolean).sort((a, b) => b.score - a.score); + return results.sort((a, b) => b.score - a.score); } async function pollRootme() {