root-me: add logins

This commit is contained in:
Lopinosaurus 2026-03-11 23:05:56 +01:00
parent 7686779fbb
commit 59ae200324
3 changed files with 145 additions and 12 deletions

113
README.md Normal file
View File

@ -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=<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
```
<scope>: <short description>
```
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 `<section class="widget" id="widget-xxx">` 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

View File

@ -1,3 +1,10 @@
724433 724433
546528 546528
709915
1078733
1060488
967844
716070
1071634
785590
963602

View File

@ -146,31 +146,44 @@ const ROOTME_POLL_MS = 10 * 60 * 1000;
let rootmeCache = null; let rootmeCache = null;
let rootmePrevScores = {}; // login → last known score 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) { async function fetchRootmeRanking(apiKey) {
const raw = fs.readFileSync(path.resolve('logins.txt'), 'utf8'); const raw = fs.readFileSync(path.resolve('logins.txt'), 'utf8');
const ids = raw.split('\n').map(l => l.trim()).filter(Boolean); const ids = raw.split('\n').map(l => l.trim()).filter(Boolean);
const headers = { 'Cookie': `api_key=${apiKey}`, 'User-Agent': 'CyberDashboard/1.0' }; 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 { try {
const resp = await fetch( const resp = await fetch(
`https://api.www.root-me.org/auteurs/${id}`, `https://api.www.root-me.org/auteurs/${id}`,
{ headers, timeout: 10000 } { headers, timeout: 10000 }
); );
if (resp.status === 429) throw new Error('rate-limited'); if (resp.status === 429) {
const profile = await resp.json(); console.warn(`[rootme] rate-limited on id "${id}", using cached value`);
const profileRaw = Array.isArray(profile) ? profile[0] : profile; if (rootmePlayerCache[id]) results.push(rootmePlayerCache[id]);
const user = profileRaw?.['0'] ?? profileRaw; } else {
if (!user || user.error) return null; const profile = await resp.json();
const profileRaw = Array.isArray(profile) ? profile[0] : profile;
return { login: user.nom || id, score: Number(user.score) || 0, rank: user.position || null }; 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) { } catch (err) {
console.error(`[rootme] fetch error for id "${id}":`, err.message); 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() { async function pollRootme() {