feat: centralized API dashboard + Docker container
API server (api/):
- Node.js + Express + SQLite (better-sqlite3, WAL mode)
- POST /api/v1/submit — receive blocks from WP sites (rate limited 30/min/IP)
- GET /api/v1/stats — public aggregated stats with 30s cache
- GET /api/v1/stream — SSE live feed, pushed every 2s
- GET /api/v1/health — health check
- IP masking: only first 2 octets stored (192.168.x.x)
- UA family detection: curl, Python, Go, bots, Chrome, etc.
- docker-compose.yml with named volume for SQLite persistence
Dashboard (api/public/index.html):
- Hacker/terminal aesthetic: black + matrix green, CRT scanlines
- Live stat cards: total blocked, today, 7d, 30d, sites reporting
- Canvas 24h activity trend chart with gradient bars
- CSS bar charts: form types, bot toolkit, block reasons
- Live SSE threat feed with countUp animation and auto-scroll
- Top 10 attackers table with frequency bars
- Polls /api/v1/stats every 6s, SSE for instant feed updates
WordPress plugin (honeypot-fields.php):
- SmartHoneypotAPIClient: queue (WP option) + WP-cron batch flush every 5min
- log_spam() now enqueues to central API after local DB write
- Admin 'Central API' tab: enable toggle, endpoint URL, sync stats, manual flush
- Cron properly registered/deregistered on activate/deactivate
2026-03-09 19:21:41 +01:00
|
|
|
|
'use strict';
|
|
|
|
|
|
|
2026-03-09 19:26:23 +01:00
|
|
|
|
const express = require('express');
|
|
|
|
|
|
const Database = require('better-sqlite3');
|
|
|
|
|
|
const path = require('path');
|
fix: CF7 bypass, auto-flush, layout, contrast, IP geo v2.4.0
CF7:
- Add wpcf7_spam filter registered before is_admin() early-return so
CF7 AJAX submissions (admin-ajax.php) are properly validated
- Exclude CF7 posts from generic catch-all (prevent double-checking)
Auto-flush:
- Add maybe_flush_overdue() with 5-min transient lock, hooked to
shutdown action so every PHP request can trigger a flush if overdue
- No longer depends solely on WP-Cron firing
Dashboard layout:
- Top Attackers moved into right column below live feed
- Viewport-fill layout: body/main use flex+overflow:hidden so content
stays in view; left col scrolls independently if needed
- Feed panel takes flex:1, attackers panel capped at 260px
Colors:
- --dim: #006600 → #44bb77 (legible secondary text, ~5:1 contrast)
- --dim2: #228844 added for slightly darker secondary use
- --muted kept dark for backgrounds only; border lightened slightly
IP geo (server-side, async, non-blocking):
- country + asn columns added to blocks table (migration-safe)
- enrichIP() calls ip-api.com free HTTP API per unique IP, cached 1h
- Background job enriches historic rows missing country (5 per 20s)
- Stats and live feed now include country code + ASN
- Dashboard shows country flag emoji in feed rows and attackers table
- Full AS name shown as tooltip on ASN column
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 07:57:16 +01:00
|
|
|
|
const http = require('http');
|
2026-03-09 19:26:23 +01:00
|
|
|
|
const { timingSafeEqual } = require('crypto');
|
feat: centralized API dashboard + Docker container
API server (api/):
- Node.js + Express + SQLite (better-sqlite3, WAL mode)
- POST /api/v1/submit — receive blocks from WP sites (rate limited 30/min/IP)
- GET /api/v1/stats — public aggregated stats with 30s cache
- GET /api/v1/stream — SSE live feed, pushed every 2s
- GET /api/v1/health — health check
- IP masking: only first 2 octets stored (192.168.x.x)
- UA family detection: curl, Python, Go, bots, Chrome, etc.
- docker-compose.yml with named volume for SQLite persistence
Dashboard (api/public/index.html):
- Hacker/terminal aesthetic: black + matrix green, CRT scanlines
- Live stat cards: total blocked, today, 7d, 30d, sites reporting
- Canvas 24h activity trend chart with gradient bars
- CSS bar charts: form types, bot toolkit, block reasons
- Live SSE threat feed with countUp animation and auto-scroll
- Top 10 attackers table with frequency bars
- Polls /api/v1/stats every 6s, SSE for instant feed updates
WordPress plugin (honeypot-fields.php):
- SmartHoneypotAPIClient: queue (WP option) + WP-cron batch flush every 5min
- log_spam() now enqueues to central API after local DB write
- Admin 'Central API' tab: enable toggle, endpoint URL, sync stats, manual flush
- Cron properly registered/deregistered on activate/deactivate
2026-03-09 19:21:41 +01:00
|
|
|
|
|
|
|
|
|
|
const app = express();
|
|
|
|
|
|
const PORT = Number(process.env.PORT) || 3000;
|
|
|
|
|
|
const DB = new Database(process.env.DB_PATH || '/data/honeypot.db');
|
|
|
|
|
|
|
|
|
|
|
|
// ── Database setup ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
DB.pragma('journal_mode = WAL');
|
|
|
|
|
|
DB.pragma('synchronous = NORMAL');
|
|
|
|
|
|
DB.pragma('cache_size = -8000');
|
|
|
|
|
|
|
|
|
|
|
|
DB.exec(`
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS blocks (
|
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
|
received_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
|
|
|
|
site_id TEXT NOT NULL DEFAULT '',
|
|
|
|
|
|
ip_masked TEXT NOT NULL DEFAULT '',
|
|
|
|
|
|
form_type TEXT NOT NULL DEFAULT '',
|
|
|
|
|
|
reason TEXT NOT NULL DEFAULT '',
|
fix: CF7 bypass, auto-flush, layout, contrast, IP geo v2.4.0
CF7:
- Add wpcf7_spam filter registered before is_admin() early-return so
CF7 AJAX submissions (admin-ajax.php) are properly validated
- Exclude CF7 posts from generic catch-all (prevent double-checking)
Auto-flush:
- Add maybe_flush_overdue() with 5-min transient lock, hooked to
shutdown action so every PHP request can trigger a flush if overdue
- No longer depends solely on WP-Cron firing
Dashboard layout:
- Top Attackers moved into right column below live feed
- Viewport-fill layout: body/main use flex+overflow:hidden so content
stays in view; left col scrolls independently if needed
- Feed panel takes flex:1, attackers panel capped at 260px
Colors:
- --dim: #006600 → #44bb77 (legible secondary text, ~5:1 contrast)
- --dim2: #228844 added for slightly darker secondary use
- --muted kept dark for backgrounds only; border lightened slightly
IP geo (server-side, async, non-blocking):
- country + asn columns added to blocks table (migration-safe)
- enrichIP() calls ip-api.com free HTTP API per unique IP, cached 1h
- Background job enriches historic rows missing country (5 per 20s)
- Stats and live feed now include country code + ASN
- Dashboard shows country flag emoji in feed rows and attackers table
- Full AS name shown as tooltip on ASN column
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 07:57:16 +01:00
|
|
|
|
ua_family TEXT NOT NULL DEFAULT '',
|
|
|
|
|
|
country TEXT NOT NULL DEFAULT '',
|
|
|
|
|
|
asn TEXT NOT NULL DEFAULT ''
|
feat: centralized API dashboard + Docker container
API server (api/):
- Node.js + Express + SQLite (better-sqlite3, WAL mode)
- POST /api/v1/submit — receive blocks from WP sites (rate limited 30/min/IP)
- GET /api/v1/stats — public aggregated stats with 30s cache
- GET /api/v1/stream — SSE live feed, pushed every 2s
- GET /api/v1/health — health check
- IP masking: only first 2 octets stored (192.168.x.x)
- UA family detection: curl, Python, Go, bots, Chrome, etc.
- docker-compose.yml with named volume for SQLite persistence
Dashboard (api/public/index.html):
- Hacker/terminal aesthetic: black + matrix green, CRT scanlines
- Live stat cards: total blocked, today, 7d, 30d, sites reporting
- Canvas 24h activity trend chart with gradient bars
- CSS bar charts: form types, bot toolkit, block reasons
- Live SSE threat feed with countUp animation and auto-scroll
- Top 10 attackers table with frequency bars
- Polls /api/v1/stats every 6s, SSE for instant feed updates
WordPress plugin (honeypot-fields.php):
- SmartHoneypotAPIClient: queue (WP option) + WP-cron batch flush every 5min
- log_spam() now enqueues to central API after local DB write
- Admin 'Central API' tab: enable toggle, endpoint URL, sync stats, manual flush
- Cron properly registered/deregistered on activate/deactivate
2026-03-09 19:21:41 +01:00
|
|
|
|
);
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS sites (
|
|
|
|
|
|
site_id TEXT PRIMARY KEY,
|
|
|
|
|
|
first_seen INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
|
|
|
|
last_seen INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
|
|
|
|
block_count INTEGER NOT NULL DEFAULT 0
|
|
|
|
|
|
);
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_recv ON blocks(received_at DESC);
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_ip ON blocks(ip_masked);
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_site ON blocks(site_id);
|
|
|
|
|
|
`);
|
|
|
|
|
|
|
fix: CF7 bypass, auto-flush, layout, contrast, IP geo v2.4.0
CF7:
- Add wpcf7_spam filter registered before is_admin() early-return so
CF7 AJAX submissions (admin-ajax.php) are properly validated
- Exclude CF7 posts from generic catch-all (prevent double-checking)
Auto-flush:
- Add maybe_flush_overdue() with 5-min transient lock, hooked to
shutdown action so every PHP request can trigger a flush if overdue
- No longer depends solely on WP-Cron firing
Dashboard layout:
- Top Attackers moved into right column below live feed
- Viewport-fill layout: body/main use flex+overflow:hidden so content
stays in view; left col scrolls independently if needed
- Feed panel takes flex:1, attackers panel capped at 260px
Colors:
- --dim: #006600 → #44bb77 (legible secondary text, ~5:1 contrast)
- --dim2: #228844 added for slightly darker secondary use
- --muted kept dark for backgrounds only; border lightened slightly
IP geo (server-side, async, non-blocking):
- country + asn columns added to blocks table (migration-safe)
- enrichIP() calls ip-api.com free HTTP API per unique IP, cached 1h
- Background job enriches historic rows missing country (5 per 20s)
- Stats and live feed now include country code + ASN
- Dashboard shows country flag emoji in feed rows and attackers table
- Full AS name shown as tooltip on ASN column
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 07:57:16 +01:00
|
|
|
|
// Add country/asn columns to existing tables (migration — ignored if already present)
|
|
|
|
|
|
try { DB.exec(`ALTER TABLE blocks ADD COLUMN country TEXT NOT NULL DEFAULT ''`); } catch {}
|
|
|
|
|
|
try { DB.exec(`ALTER TABLE blocks ADD COLUMN asn TEXT NOT NULL DEFAULT ''`); } catch {}
|
|
|
|
|
|
|
2026-03-09 19:26:23 +01:00
|
|
|
|
// ── Auth token ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
const API_TOKEN = (process.env.API_TOKEN || '').trim();
|
|
|
|
|
|
|
|
|
|
|
|
function requireToken(req, res, next) {
|
|
|
|
|
|
if (!API_TOKEN) return next(); // dev: no token set = open
|
|
|
|
|
|
|
|
|
|
|
|
const auth = req.headers['authorization'] || '';
|
|
|
|
|
|
const token = auth.startsWith('Bearer ') ? auth.slice(7) : '';
|
|
|
|
|
|
|
|
|
|
|
|
// Constant-time comparison — pad both to 128 bytes to avoid length leaks
|
|
|
|
|
|
const a = Buffer.alloc(128); Buffer.from(token).copy(a, 0, 0, 128);
|
|
|
|
|
|
const b = Buffer.alloc(128); Buffer.from(API_TOKEN).copy(b, 0, 0, 128);
|
feat: centralized API dashboard + Docker container
API server (api/):
- Node.js + Express + SQLite (better-sqlite3, WAL mode)
- POST /api/v1/submit — receive blocks from WP sites (rate limited 30/min/IP)
- GET /api/v1/stats — public aggregated stats with 30s cache
- GET /api/v1/stream — SSE live feed, pushed every 2s
- GET /api/v1/health — health check
- IP masking: only first 2 octets stored (192.168.x.x)
- UA family detection: curl, Python, Go, bots, Chrome, etc.
- docker-compose.yml with named volume for SQLite persistence
Dashboard (api/public/index.html):
- Hacker/terminal aesthetic: black + matrix green, CRT scanlines
- Live stat cards: total blocked, today, 7d, 30d, sites reporting
- Canvas 24h activity trend chart with gradient bars
- CSS bar charts: form types, bot toolkit, block reasons
- Live SSE threat feed with countUp animation and auto-scroll
- Top 10 attackers table with frequency bars
- Polls /api/v1/stats every 6s, SSE for instant feed updates
WordPress plugin (honeypot-fields.php):
- SmartHoneypotAPIClient: queue (WP option) + WP-cron batch flush every 5min
- log_spam() now enqueues to central API after local DB write
- Admin 'Central API' tab: enable toggle, endpoint URL, sync stats, manual flush
- Cron properly registered/deregistered on activate/deactivate
2026-03-09 19:21:41 +01:00
|
|
|
|
|
2026-03-09 19:26:23 +01:00
|
|
|
|
if (!timingSafeEqual(a, b) || token !== API_TOKEN) {
|
|
|
|
|
|
return res.status(403).json({ error: 'Forbidden' });
|
feat: centralized API dashboard + Docker container
API server (api/):
- Node.js + Express + SQLite (better-sqlite3, WAL mode)
- POST /api/v1/submit — receive blocks from WP sites (rate limited 30/min/IP)
- GET /api/v1/stats — public aggregated stats with 30s cache
- GET /api/v1/stream — SSE live feed, pushed every 2s
- GET /api/v1/health — health check
- IP masking: only first 2 octets stored (192.168.x.x)
- UA family detection: curl, Python, Go, bots, Chrome, etc.
- docker-compose.yml with named volume for SQLite persistence
Dashboard (api/public/index.html):
- Hacker/terminal aesthetic: black + matrix green, CRT scanlines
- Live stat cards: total blocked, today, 7d, 30d, sites reporting
- Canvas 24h activity trend chart with gradient bars
- CSS bar charts: form types, bot toolkit, block reasons
- Live SSE threat feed with countUp animation and auto-scroll
- Top 10 attackers table with frequency bars
- Polls /api/v1/stats every 6s, SSE for instant feed updates
WordPress plugin (honeypot-fields.php):
- SmartHoneypotAPIClient: queue (WP option) + WP-cron batch flush every 5min
- log_spam() now enqueues to central API after local DB write
- Admin 'Central API' tab: enable toggle, endpoint URL, sync stats, manual flush
- Cron properly registered/deregistered on activate/deactivate
2026-03-09 19:21:41 +01:00
|
|
|
|
}
|
2026-03-09 19:26:23 +01:00
|
|
|
|
next();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
function sanitizeIP(ip = '') {
|
|
|
|
|
|
return String(ip).trim().slice(0, 45) || '?';
|
feat: centralized API dashboard + Docker container
API server (api/):
- Node.js + Express + SQLite (better-sqlite3, WAL mode)
- POST /api/v1/submit — receive blocks from WP sites (rate limited 30/min/IP)
- GET /api/v1/stats — public aggregated stats with 30s cache
- GET /api/v1/stream — SSE live feed, pushed every 2s
- GET /api/v1/health — health check
- IP masking: only first 2 octets stored (192.168.x.x)
- UA family detection: curl, Python, Go, bots, Chrome, etc.
- docker-compose.yml with named volume for SQLite persistence
Dashboard (api/public/index.html):
- Hacker/terminal aesthetic: black + matrix green, CRT scanlines
- Live stat cards: total blocked, today, 7d, 30d, sites reporting
- Canvas 24h activity trend chart with gradient bars
- CSS bar charts: form types, bot toolkit, block reasons
- Live SSE threat feed with countUp animation and auto-scroll
- Top 10 attackers table with frequency bars
- Polls /api/v1/stats every 6s, SSE for instant feed updates
WordPress plugin (honeypot-fields.php):
- SmartHoneypotAPIClient: queue (WP option) + WP-cron batch flush every 5min
- log_spam() now enqueues to central API after local DB write
- Admin 'Central API' tab: enable toggle, endpoint URL, sync stats, manual flush
- Cron properly registered/deregistered on activate/deactivate
2026-03-09 19:21:41 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const UA_MAP = [
|
|
|
|
|
|
[/curl\//i, 'curl'],
|
|
|
|
|
|
[/python-requests|python\//i, 'Python'],
|
|
|
|
|
|
[/go-http-client/i, 'Go'],
|
|
|
|
|
|
[/wget\//i, 'Wget'],
|
|
|
|
|
|
[/java\//i, 'Java'],
|
|
|
|
|
|
[/ruby/i, 'Ruby'],
|
|
|
|
|
|
[/perl\//i, 'Perl'],
|
|
|
|
|
|
[/php\//i, 'PHP'],
|
|
|
|
|
|
[/scrapy/i, 'Scrapy'],
|
|
|
|
|
|
[/postman/i, 'Postman'],
|
|
|
|
|
|
[/axios/i, 'Axios'],
|
|
|
|
|
|
[/node-fetch|node\.js/i, 'Node.js'],
|
|
|
|
|
|
[/headlesschrome|phantomjs/i, 'Headless Browser'],
|
|
|
|
|
|
[/(bot|crawler|spider|slurp)/i, 'Bot/Crawler'],
|
|
|
|
|
|
[/chrome/i, 'Chrome'],
|
|
|
|
|
|
[/firefox/i, 'Firefox'],
|
|
|
|
|
|
[/safari/i, 'Safari'],
|
|
|
|
|
|
[/edge|edg\//i, 'Edge'],
|
|
|
|
|
|
[/opera|opr\//i, 'Opera'],
|
|
|
|
|
|
[/msie|trident/i, 'IE'],
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
function parseUA(ua = '') {
|
|
|
|
|
|
for (const [re, label] of UA_MAP) if (re.test(ua)) return label;
|
|
|
|
|
|
return ua.length ? 'Other' : 'No UA';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
fix: CF7 bypass, auto-flush, layout, contrast, IP geo v2.4.0
CF7:
- Add wpcf7_spam filter registered before is_admin() early-return so
CF7 AJAX submissions (admin-ajax.php) are properly validated
- Exclude CF7 posts from generic catch-all (prevent double-checking)
Auto-flush:
- Add maybe_flush_overdue() with 5-min transient lock, hooked to
shutdown action so every PHP request can trigger a flush if overdue
- No longer depends solely on WP-Cron firing
Dashboard layout:
- Top Attackers moved into right column below live feed
- Viewport-fill layout: body/main use flex+overflow:hidden so content
stays in view; left col scrolls independently if needed
- Feed panel takes flex:1, attackers panel capped at 260px
Colors:
- --dim: #006600 → #44bb77 (legible secondary text, ~5:1 contrast)
- --dim2: #228844 added for slightly darker secondary use
- --muted kept dark for backgrounds only; border lightened slightly
IP geo (server-side, async, non-blocking):
- country + asn columns added to blocks table (migration-safe)
- enrichIP() calls ip-api.com free HTTP API per unique IP, cached 1h
- Background job enriches historic rows missing country (5 per 20s)
- Stats and live feed now include country code + ASN
- Dashboard shows country flag emoji in feed rows and attackers table
- Full AS name shown as tooltip on ASN column
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 07:57:16 +01:00
|
|
|
|
// ── IP geo-enrichment (ip-api.com free tier, HTTP) ────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
const stmtEnrich = DB.prepare('UPDATE blocks SET country=?, asn=? WHERE id=?');
|
|
|
|
|
|
const enrichCache = new Map(); // ip -> expiry ms (deduplicate within 1h)
|
|
|
|
|
|
|
|
|
|
|
|
function isPrivateIP(ip) {
|
|
|
|
|
|
return /^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|127\.|::1$|fc|fd)/.test(ip);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function enrichIP(rowId, ip) {
|
|
|
|
|
|
if (!ip || ip === '?' || isPrivateIP(ip)) return;
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
|
if ((enrichCache.get(ip) || 0) > now) return; // recently done
|
|
|
|
|
|
enrichCache.set(ip, now + 3_600_000); // cache 1 h
|
|
|
|
|
|
|
|
|
|
|
|
http.get(
|
|
|
|
|
|
`http://ip-api.com/json/${encodeURIComponent(ip)}?fields=status,countryCode,as`,
|
|
|
|
|
|
{ timeout: 5000 },
|
|
|
|
|
|
res => {
|
|
|
|
|
|
let data = '';
|
|
|
|
|
|
res.on('data', d => data += d);
|
|
|
|
|
|
res.on('end', () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const j = JSON.parse(data);
|
|
|
|
|
|
if (j.status === 'success') {
|
|
|
|
|
|
stmtEnrich.run(
|
|
|
|
|
|
(j.countryCode || '').slice(0, 2),
|
|
|
|
|
|
(j.as || '').slice(0, 50),
|
|
|
|
|
|
rowId
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
).on('error', () => enrichCache.delete(ip)); // retry next time
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Background enrichment: fill any rows missing country (e.g. from history import)
|
|
|
|
|
|
const stmtUnenriched = DB.prepare(
|
|
|
|
|
|
"SELECT id, ip_masked FROM blocks WHERE country='' AND ip_masked != '' AND ip_masked != '?' LIMIT 5"
|
|
|
|
|
|
);
|
|
|
|
|
|
setInterval(() => {
|
|
|
|
|
|
for (const row of stmtUnenriched.all()) enrichIP(row.id, row.ip_masked);
|
|
|
|
|
|
}, 20_000);
|
|
|
|
|
|
|
feat: centralized API dashboard + Docker container
API server (api/):
- Node.js + Express + SQLite (better-sqlite3, WAL mode)
- POST /api/v1/submit — receive blocks from WP sites (rate limited 30/min/IP)
- GET /api/v1/stats — public aggregated stats with 30s cache
- GET /api/v1/stream — SSE live feed, pushed every 2s
- GET /api/v1/health — health check
- IP masking: only first 2 octets stored (192.168.x.x)
- UA family detection: curl, Python, Go, bots, Chrome, etc.
- docker-compose.yml with named volume for SQLite persistence
Dashboard (api/public/index.html):
- Hacker/terminal aesthetic: black + matrix green, CRT scanlines
- Live stat cards: total blocked, today, 7d, 30d, sites reporting
- Canvas 24h activity trend chart with gradient bars
- CSS bar charts: form types, bot toolkit, block reasons
- Live SSE threat feed with countUp animation and auto-scroll
- Top 10 attackers table with frequency bars
- Polls /api/v1/stats every 6s, SSE for instant feed updates
WordPress plugin (honeypot-fields.php):
- SmartHoneypotAPIClient: queue (WP option) + WP-cron batch flush every 5min
- log_spam() now enqueues to central API after local DB write
- Admin 'Central API' tab: enable toggle, endpoint URL, sync stats, manual flush
- Cron properly registered/deregistered on activate/deactivate
2026-03-09 19:21:41 +01:00
|
|
|
|
// ── In-memory rate limiter ────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
const rl = new Map();
|
|
|
|
|
|
setInterval(() => { const n = Date.now(); for (const [k, v] of rl) if (n > v.r) rl.delete(k); }, 30_000);
|
|
|
|
|
|
|
|
|
|
|
|
function allowed(ip, max = 30, win = 60_000) {
|
|
|
|
|
|
const n = Date.now();
|
|
|
|
|
|
let e = rl.get(ip);
|
|
|
|
|
|
if (!e || n > e.r) { e = { c: 0, r: n + win }; rl.set(ip, e); }
|
|
|
|
|
|
return ++e.c <= max;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Stats cache (30s TTL) ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
let _cache = null, _cacheTs = 0;
|
|
|
|
|
|
|
|
|
|
|
|
function getStats() {
|
|
|
|
|
|
if (_cache && Date.now() - _cacheTs < 30_000) return _cache;
|
|
|
|
|
|
const now = Math.floor(Date.now() / 1000);
|
|
|
|
|
|
|
|
|
|
|
|
_cache = {
|
|
|
|
|
|
total: DB.prepare('SELECT COUNT(*) n FROM blocks').get().n,
|
|
|
|
|
|
today: DB.prepare('SELECT COUNT(*) n FROM blocks WHERE received_at > ?').get(now - 86400).n,
|
|
|
|
|
|
last_7d: DB.prepare('SELECT COUNT(*) n FROM blocks WHERE received_at > ?').get(now - 604800).n,
|
|
|
|
|
|
last_30d: DB.prepare('SELECT COUNT(*) n FROM blocks WHERE received_at > ?').get(now - 2592000).n,
|
|
|
|
|
|
total_sites: DB.prepare('SELECT COUNT(*) n FROM sites').get().n,
|
|
|
|
|
|
top_ips: DB.prepare(`
|
fix: CF7 bypass, auto-flush, layout, contrast, IP geo v2.4.0
CF7:
- Add wpcf7_spam filter registered before is_admin() early-return so
CF7 AJAX submissions (admin-ajax.php) are properly validated
- Exclude CF7 posts from generic catch-all (prevent double-checking)
Auto-flush:
- Add maybe_flush_overdue() with 5-min transient lock, hooked to
shutdown action so every PHP request can trigger a flush if overdue
- No longer depends solely on WP-Cron firing
Dashboard layout:
- Top Attackers moved into right column below live feed
- Viewport-fill layout: body/main use flex+overflow:hidden so content
stays in view; left col scrolls independently if needed
- Feed panel takes flex:1, attackers panel capped at 260px
Colors:
- --dim: #006600 → #44bb77 (legible secondary text, ~5:1 contrast)
- --dim2: #228844 added for slightly darker secondary use
- --muted kept dark for backgrounds only; border lightened slightly
IP geo (server-side, async, non-blocking):
- country + asn columns added to blocks table (migration-safe)
- enrichIP() calls ip-api.com free HTTP API per unique IP, cached 1h
- Background job enriches historic rows missing country (5 per 20s)
- Stats and live feed now include country code + ASN
- Dashboard shows country flag emoji in feed rows and attackers table
- Full AS name shown as tooltip on ASN column
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 07:57:16 +01:00
|
|
|
|
SELECT ip_masked ip, country, asn, COUNT(*) hits
|
feat: centralized API dashboard + Docker container
API server (api/):
- Node.js + Express + SQLite (better-sqlite3, WAL mode)
- POST /api/v1/submit — receive blocks from WP sites (rate limited 30/min/IP)
- GET /api/v1/stats — public aggregated stats with 30s cache
- GET /api/v1/stream — SSE live feed, pushed every 2s
- GET /api/v1/health — health check
- IP masking: only first 2 octets stored (192.168.x.x)
- UA family detection: curl, Python, Go, bots, Chrome, etc.
- docker-compose.yml with named volume for SQLite persistence
Dashboard (api/public/index.html):
- Hacker/terminal aesthetic: black + matrix green, CRT scanlines
- Live stat cards: total blocked, today, 7d, 30d, sites reporting
- Canvas 24h activity trend chart with gradient bars
- CSS bar charts: form types, bot toolkit, block reasons
- Live SSE threat feed with countUp animation and auto-scroll
- Top 10 attackers table with frequency bars
- Polls /api/v1/stats every 6s, SSE for instant feed updates
WordPress plugin (honeypot-fields.php):
- SmartHoneypotAPIClient: queue (WP option) + WP-cron batch flush every 5min
- log_spam() now enqueues to central API after local DB write
- Admin 'Central API' tab: enable toggle, endpoint URL, sync stats, manual flush
- Cron properly registered/deregistered on activate/deactivate
2026-03-09 19:21:41 +01:00
|
|
|
|
FROM blocks WHERE received_at > ?
|
|
|
|
|
|
GROUP BY ip_masked ORDER BY hits DESC LIMIT 10
|
|
|
|
|
|
`).all(now - 2592000),
|
|
|
|
|
|
top_forms: DB.prepare(`
|
|
|
|
|
|
SELECT form_type, COUNT(*) hits
|
|
|
|
|
|
FROM blocks WHERE received_at > ?
|
|
|
|
|
|
GROUP BY form_type ORDER BY hits DESC LIMIT 8
|
|
|
|
|
|
`).all(now - 2592000),
|
|
|
|
|
|
top_reasons: DB.prepare(`
|
|
|
|
|
|
SELECT reason, COUNT(*) hits
|
|
|
|
|
|
FROM blocks WHERE received_at > ?
|
|
|
|
|
|
GROUP BY reason ORDER BY hits DESC LIMIT 8
|
|
|
|
|
|
`).all(now - 2592000),
|
|
|
|
|
|
top_ua: DB.prepare(`
|
|
|
|
|
|
SELECT ua_family, COUNT(*) hits
|
|
|
|
|
|
FROM blocks WHERE received_at > ?
|
|
|
|
|
|
GROUP BY ua_family ORDER BY hits DESC LIMIT 8
|
|
|
|
|
|
`).all(now - 2592000),
|
|
|
|
|
|
recent: DB.prepare(`
|
fix: CF7 bypass, auto-flush, layout, contrast, IP geo v2.4.0
CF7:
- Add wpcf7_spam filter registered before is_admin() early-return so
CF7 AJAX submissions (admin-ajax.php) are properly validated
- Exclude CF7 posts from generic catch-all (prevent double-checking)
Auto-flush:
- Add maybe_flush_overdue() with 5-min transient lock, hooked to
shutdown action so every PHP request can trigger a flush if overdue
- No longer depends solely on WP-Cron firing
Dashboard layout:
- Top Attackers moved into right column below live feed
- Viewport-fill layout: body/main use flex+overflow:hidden so content
stays in view; left col scrolls independently if needed
- Feed panel takes flex:1, attackers panel capped at 260px
Colors:
- --dim: #006600 → #44bb77 (legible secondary text, ~5:1 contrast)
- --dim2: #228844 added for slightly darker secondary use
- --muted kept dark for backgrounds only; border lightened slightly
IP geo (server-side, async, non-blocking):
- country + asn columns added to blocks table (migration-safe)
- enrichIP() calls ip-api.com free HTTP API per unique IP, cached 1h
- Background job enriches historic rows missing country (5 per 20s)
- Stats and live feed now include country code + ASN
- Dashboard shows country flag emoji in feed rows and attackers table
- Full AS name shown as tooltip on ASN column
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 07:57:16 +01:00
|
|
|
|
SELECT received_at, ip_masked ip, country, form_type, reason, ua_family
|
feat: centralized API dashboard + Docker container
API server (api/):
- Node.js + Express + SQLite (better-sqlite3, WAL mode)
- POST /api/v1/submit — receive blocks from WP sites (rate limited 30/min/IP)
- GET /api/v1/stats — public aggregated stats with 30s cache
- GET /api/v1/stream — SSE live feed, pushed every 2s
- GET /api/v1/health — health check
- IP masking: only first 2 octets stored (192.168.x.x)
- UA family detection: curl, Python, Go, bots, Chrome, etc.
- docker-compose.yml with named volume for SQLite persistence
Dashboard (api/public/index.html):
- Hacker/terminal aesthetic: black + matrix green, CRT scanlines
- Live stat cards: total blocked, today, 7d, 30d, sites reporting
- Canvas 24h activity trend chart with gradient bars
- CSS bar charts: form types, bot toolkit, block reasons
- Live SSE threat feed with countUp animation and auto-scroll
- Top 10 attackers table with frequency bars
- Polls /api/v1/stats every 6s, SSE for instant feed updates
WordPress plugin (honeypot-fields.php):
- SmartHoneypotAPIClient: queue (WP option) + WP-cron batch flush every 5min
- log_spam() now enqueues to central API after local DB write
- Admin 'Central API' tab: enable toggle, endpoint URL, sync stats, manual flush
- Cron properly registered/deregistered on activate/deactivate
2026-03-09 19:21:41 +01:00
|
|
|
|
FROM blocks ORDER BY id DESC LIMIT 40
|
|
|
|
|
|
`).all(),
|
|
|
|
|
|
hourly: DB.prepare(`
|
|
|
|
|
|
SELECT (received_at / 3600) * 3600 h, COUNT(*) n
|
|
|
|
|
|
FROM blocks WHERE received_at > ?
|
|
|
|
|
|
GROUP BY h ORDER BY h ASC
|
|
|
|
|
|
`).all(now - 86400),
|
|
|
|
|
|
};
|
|
|
|
|
|
_cacheTs = Date.now();
|
|
|
|
|
|
return _cache;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── SSE live stream ───────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
const sseClients = new Set();
|
|
|
|
|
|
let lastId = DB.prepare('SELECT MAX(id) id FROM blocks').get().id || 0;
|
|
|
|
|
|
|
|
|
|
|
|
setInterval(() => {
|
|
|
|
|
|
if (!sseClients.size) return;
|
|
|
|
|
|
const rows = DB.prepare('SELECT * FROM blocks WHERE id > ? ORDER BY id ASC LIMIT 20').all(lastId);
|
|
|
|
|
|
if (!rows.length) return;
|
|
|
|
|
|
lastId = rows.at(-1).id;
|
|
|
|
|
|
const msg = `data: ${JSON.stringify(rows)}\n\n`;
|
|
|
|
|
|
for (const r of sseClients) { try { r.write(msg); } catch { sseClients.delete(r); } }
|
|
|
|
|
|
}, 2000);
|
|
|
|
|
|
|
|
|
|
|
|
// ── Prepared statements ───────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
const stmtIns = DB.prepare(
|
fix: CF7 bypass, auto-flush, layout, contrast, IP geo v2.4.0
CF7:
- Add wpcf7_spam filter registered before is_admin() early-return so
CF7 AJAX submissions (admin-ajax.php) are properly validated
- Exclude CF7 posts from generic catch-all (prevent double-checking)
Auto-flush:
- Add maybe_flush_overdue() with 5-min transient lock, hooked to
shutdown action so every PHP request can trigger a flush if overdue
- No longer depends solely on WP-Cron firing
Dashboard layout:
- Top Attackers moved into right column below live feed
- Viewport-fill layout: body/main use flex+overflow:hidden so content
stays in view; left col scrolls independently if needed
- Feed panel takes flex:1, attackers panel capped at 260px
Colors:
- --dim: #006600 → #44bb77 (legible secondary text, ~5:1 contrast)
- --dim2: #228844 added for slightly darker secondary use
- --muted kept dark for backgrounds only; border lightened slightly
IP geo (server-side, async, non-blocking):
- country + asn columns added to blocks table (migration-safe)
- enrichIP() calls ip-api.com free HTTP API per unique IP, cached 1h
- Background job enriches historic rows missing country (5 per 20s)
- Stats and live feed now include country code + ASN
- Dashboard shows country flag emoji in feed rows and attackers table
- Full AS name shown as tooltip on ASN column
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 07:57:16 +01:00
|
|
|
|
'INSERT INTO blocks (received_at, site_id, ip_masked, form_type, reason, ua_family, country, asn) VALUES (?,?,?,?,?,?,?,?)'
|
feat: centralized API dashboard + Docker container
API server (api/):
- Node.js + Express + SQLite (better-sqlite3, WAL mode)
- POST /api/v1/submit — receive blocks from WP sites (rate limited 30/min/IP)
- GET /api/v1/stats — public aggregated stats with 30s cache
- GET /api/v1/stream — SSE live feed, pushed every 2s
- GET /api/v1/health — health check
- IP masking: only first 2 octets stored (192.168.x.x)
- UA family detection: curl, Python, Go, bots, Chrome, etc.
- docker-compose.yml with named volume for SQLite persistence
Dashboard (api/public/index.html):
- Hacker/terminal aesthetic: black + matrix green, CRT scanlines
- Live stat cards: total blocked, today, 7d, 30d, sites reporting
- Canvas 24h activity trend chart with gradient bars
- CSS bar charts: form types, bot toolkit, block reasons
- Live SSE threat feed with countUp animation and auto-scroll
- Top 10 attackers table with frequency bars
- Polls /api/v1/stats every 6s, SSE for instant feed updates
WordPress plugin (honeypot-fields.php):
- SmartHoneypotAPIClient: queue (WP option) + WP-cron batch flush every 5min
- log_spam() now enqueues to central API after local DB write
- Admin 'Central API' tab: enable toggle, endpoint URL, sync stats, manual flush
- Cron properly registered/deregistered on activate/deactivate
2026-03-09 19:21:41 +01:00
|
|
|
|
);
|
|
|
|
|
|
const stmtSite = DB.prepare(`
|
|
|
|
|
|
INSERT INTO sites (site_id, first_seen, last_seen, block_count) VALUES (?,?,?,?)
|
|
|
|
|
|
ON CONFLICT(site_id) DO UPDATE SET
|
|
|
|
|
|
last_seen = excluded.last_seen,
|
|
|
|
|
|
block_count = block_count + excluded.block_count
|
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
|
|
const insertBatch = DB.transaction((siteId, blocks) => {
|
|
|
|
|
|
const now = Math.floor(Date.now() / 1000);
|
fix: CF7 bypass, auto-flush, layout, contrast, IP geo v2.4.0
CF7:
- Add wpcf7_spam filter registered before is_admin() early-return so
CF7 AJAX submissions (admin-ajax.php) are properly validated
- Exclude CF7 posts from generic catch-all (prevent double-checking)
Auto-flush:
- Add maybe_flush_overdue() with 5-min transient lock, hooked to
shutdown action so every PHP request can trigger a flush if overdue
- No longer depends solely on WP-Cron firing
Dashboard layout:
- Top Attackers moved into right column below live feed
- Viewport-fill layout: body/main use flex+overflow:hidden so content
stays in view; left col scrolls independently if needed
- Feed panel takes flex:1, attackers panel capped at 260px
Colors:
- --dim: #006600 → #44bb77 (legible secondary text, ~5:1 contrast)
- --dim2: #228844 added for slightly darker secondary use
- --muted kept dark for backgrounds only; border lightened slightly
IP geo (server-side, async, non-blocking):
- country + asn columns added to blocks table (migration-safe)
- enrichIP() calls ip-api.com free HTTP API per unique IP, cached 1h
- Background job enriches historic rows missing country (5 per 20s)
- Stats and live feed now include country code + ASN
- Dashboard shows country flag emoji in feed rows and attackers table
- Full AS name shown as tooltip on ASN column
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 07:57:16 +01:00
|
|
|
|
const ids = [];
|
feat: centralized API dashboard + Docker container
API server (api/):
- Node.js + Express + SQLite (better-sqlite3, WAL mode)
- POST /api/v1/submit — receive blocks from WP sites (rate limited 30/min/IP)
- GET /api/v1/stats — public aggregated stats with 30s cache
- GET /api/v1/stream — SSE live feed, pushed every 2s
- GET /api/v1/health — health check
- IP masking: only first 2 octets stored (192.168.x.x)
- UA family detection: curl, Python, Go, bots, Chrome, etc.
- docker-compose.yml with named volume for SQLite persistence
Dashboard (api/public/index.html):
- Hacker/terminal aesthetic: black + matrix green, CRT scanlines
- Live stat cards: total blocked, today, 7d, 30d, sites reporting
- Canvas 24h activity trend chart with gradient bars
- CSS bar charts: form types, bot toolkit, block reasons
- Live SSE threat feed with countUp animation and auto-scroll
- Top 10 attackers table with frequency bars
- Polls /api/v1/stats every 6s, SSE for instant feed updates
WordPress plugin (honeypot-fields.php):
- SmartHoneypotAPIClient: queue (WP option) + WP-cron batch flush every 5min
- log_spam() now enqueues to central API after local DB write
- Admin 'Central API' tab: enable toggle, endpoint URL, sync stats, manual flush
- Cron properly registered/deregistered on activate/deactivate
2026-03-09 19:21:41 +01:00
|
|
|
|
for (const b of blocks) {
|
fix: CF7 bypass, auto-flush, layout, contrast, IP geo v2.4.0
CF7:
- Add wpcf7_spam filter registered before is_admin() early-return so
CF7 AJAX submissions (admin-ajax.php) are properly validated
- Exclude CF7 posts from generic catch-all (prevent double-checking)
Auto-flush:
- Add maybe_flush_overdue() with 5-min transient lock, hooked to
shutdown action so every PHP request can trigger a flush if overdue
- No longer depends solely on WP-Cron firing
Dashboard layout:
- Top Attackers moved into right column below live feed
- Viewport-fill layout: body/main use flex+overflow:hidden so content
stays in view; left col scrolls independently if needed
- Feed panel takes flex:1, attackers panel capped at 260px
Colors:
- --dim: #006600 → #44bb77 (legible secondary text, ~5:1 contrast)
- --dim2: #228844 added for slightly darker secondary use
- --muted kept dark for backgrounds only; border lightened slightly
IP geo (server-side, async, non-blocking):
- country + asn columns added to blocks table (migration-safe)
- enrichIP() calls ip-api.com free HTTP API per unique IP, cached 1h
- Background job enriches historic rows missing country (5 per 20s)
- Stats and live feed now include country code + ASN
- Dashboard shows country flag emoji in feed rows and attackers table
- Full AS name shown as tooltip on ASN column
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 07:57:16 +01:00
|
|
|
|
const ts = b.blocked_at ? Math.floor(new Date(b.blocked_at) / 1000) : now;
|
|
|
|
|
|
const ip = sanitizeIP(b.ip);
|
|
|
|
|
|
const r = stmtIns.run(
|
|
|
|
|
|
ts, siteId, ip,
|
feat: centralized API dashboard + Docker container
API server (api/):
- Node.js + Express + SQLite (better-sqlite3, WAL mode)
- POST /api/v1/submit — receive blocks from WP sites (rate limited 30/min/IP)
- GET /api/v1/stats — public aggregated stats with 30s cache
- GET /api/v1/stream — SSE live feed, pushed every 2s
- GET /api/v1/health — health check
- IP masking: only first 2 octets stored (192.168.x.x)
- UA family detection: curl, Python, Go, bots, Chrome, etc.
- docker-compose.yml with named volume for SQLite persistence
Dashboard (api/public/index.html):
- Hacker/terminal aesthetic: black + matrix green, CRT scanlines
- Live stat cards: total blocked, today, 7d, 30d, sites reporting
- Canvas 24h activity trend chart with gradient bars
- CSS bar charts: form types, bot toolkit, block reasons
- Live SSE threat feed with countUp animation and auto-scroll
- Top 10 attackers table with frequency bars
- Polls /api/v1/stats every 6s, SSE for instant feed updates
WordPress plugin (honeypot-fields.php):
- SmartHoneypotAPIClient: queue (WP option) + WP-cron batch flush every 5min
- log_spam() now enqueues to central API after local DB write
- Admin 'Central API' tab: enable toggle, endpoint URL, sync stats, manual flush
- Cron properly registered/deregistered on activate/deactivate
2026-03-09 19:21:41 +01:00
|
|
|
|
String(b.form_type || '').slice(0, 100),
|
|
|
|
|
|
String(b.reason || '').slice(0, 255),
|
fix: CF7 bypass, auto-flush, layout, contrast, IP geo v2.4.0
CF7:
- Add wpcf7_spam filter registered before is_admin() early-return so
CF7 AJAX submissions (admin-ajax.php) are properly validated
- Exclude CF7 posts from generic catch-all (prevent double-checking)
Auto-flush:
- Add maybe_flush_overdue() with 5-min transient lock, hooked to
shutdown action so every PHP request can trigger a flush if overdue
- No longer depends solely on WP-Cron firing
Dashboard layout:
- Top Attackers moved into right column below live feed
- Viewport-fill layout: body/main use flex+overflow:hidden so content
stays in view; left col scrolls independently if needed
- Feed panel takes flex:1, attackers panel capped at 260px
Colors:
- --dim: #006600 → #44bb77 (legible secondary text, ~5:1 contrast)
- --dim2: #228844 added for slightly darker secondary use
- --muted kept dark for backgrounds only; border lightened slightly
IP geo (server-side, async, non-blocking):
- country + asn columns added to blocks table (migration-safe)
- enrichIP() calls ip-api.com free HTTP API per unique IP, cached 1h
- Background job enriches historic rows missing country (5 per 20s)
- Stats and live feed now include country code + ASN
- Dashboard shows country flag emoji in feed rows and attackers table
- Full AS name shown as tooltip on ASN column
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 07:57:16 +01:00
|
|
|
|
parseUA(b.user_agent || ''),
|
|
|
|
|
|
'', '' // country / asn filled async
|
feat: centralized API dashboard + Docker container
API server (api/):
- Node.js + Express + SQLite (better-sqlite3, WAL mode)
- POST /api/v1/submit — receive blocks from WP sites (rate limited 30/min/IP)
- GET /api/v1/stats — public aggregated stats with 30s cache
- GET /api/v1/stream — SSE live feed, pushed every 2s
- GET /api/v1/health — health check
- IP masking: only first 2 octets stored (192.168.x.x)
- UA family detection: curl, Python, Go, bots, Chrome, etc.
- docker-compose.yml with named volume for SQLite persistence
Dashboard (api/public/index.html):
- Hacker/terminal aesthetic: black + matrix green, CRT scanlines
- Live stat cards: total blocked, today, 7d, 30d, sites reporting
- Canvas 24h activity trend chart with gradient bars
- CSS bar charts: form types, bot toolkit, block reasons
- Live SSE threat feed with countUp animation and auto-scroll
- Top 10 attackers table with frequency bars
- Polls /api/v1/stats every 6s, SSE for instant feed updates
WordPress plugin (honeypot-fields.php):
- SmartHoneypotAPIClient: queue (WP option) + WP-cron batch flush every 5min
- log_spam() now enqueues to central API after local DB write
- Admin 'Central API' tab: enable toggle, endpoint URL, sync stats, manual flush
- Cron properly registered/deregistered on activate/deactivate
2026-03-09 19:21:41 +01:00
|
|
|
|
);
|
fix: CF7 bypass, auto-flush, layout, contrast, IP geo v2.4.0
CF7:
- Add wpcf7_spam filter registered before is_admin() early-return so
CF7 AJAX submissions (admin-ajax.php) are properly validated
- Exclude CF7 posts from generic catch-all (prevent double-checking)
Auto-flush:
- Add maybe_flush_overdue() with 5-min transient lock, hooked to
shutdown action so every PHP request can trigger a flush if overdue
- No longer depends solely on WP-Cron firing
Dashboard layout:
- Top Attackers moved into right column below live feed
- Viewport-fill layout: body/main use flex+overflow:hidden so content
stays in view; left col scrolls independently if needed
- Feed panel takes flex:1, attackers panel capped at 260px
Colors:
- --dim: #006600 → #44bb77 (legible secondary text, ~5:1 contrast)
- --dim2: #228844 added for slightly darker secondary use
- --muted kept dark for backgrounds only; border lightened slightly
IP geo (server-side, async, non-blocking):
- country + asn columns added to blocks table (migration-safe)
- enrichIP() calls ip-api.com free HTTP API per unique IP, cached 1h
- Background job enriches historic rows missing country (5 per 20s)
- Stats and live feed now include country code + ASN
- Dashboard shows country flag emoji in feed rows and attackers table
- Full AS name shown as tooltip on ASN column
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 07:57:16 +01:00
|
|
|
|
ids.push({ id: Number(r.lastInsertRowid), ip });
|
feat: centralized API dashboard + Docker container
API server (api/):
- Node.js + Express + SQLite (better-sqlite3, WAL mode)
- POST /api/v1/submit — receive blocks from WP sites (rate limited 30/min/IP)
- GET /api/v1/stats — public aggregated stats with 30s cache
- GET /api/v1/stream — SSE live feed, pushed every 2s
- GET /api/v1/health — health check
- IP masking: only first 2 octets stored (192.168.x.x)
- UA family detection: curl, Python, Go, bots, Chrome, etc.
- docker-compose.yml with named volume for SQLite persistence
Dashboard (api/public/index.html):
- Hacker/terminal aesthetic: black + matrix green, CRT scanlines
- Live stat cards: total blocked, today, 7d, 30d, sites reporting
- Canvas 24h activity trend chart with gradient bars
- CSS bar charts: form types, bot toolkit, block reasons
- Live SSE threat feed with countUp animation and auto-scroll
- Top 10 attackers table with frequency bars
- Polls /api/v1/stats every 6s, SSE for instant feed updates
WordPress plugin (honeypot-fields.php):
- SmartHoneypotAPIClient: queue (WP option) + WP-cron batch flush every 5min
- log_spam() now enqueues to central API after local DB write
- Admin 'Central API' tab: enable toggle, endpoint URL, sync stats, manual flush
- Cron properly registered/deregistered on activate/deactivate
2026-03-09 19:21:41 +01:00
|
|
|
|
}
|
|
|
|
|
|
stmtSite.run(siteId, now, now, blocks.length);
|
fix: CF7 bypass, auto-flush, layout, contrast, IP geo v2.4.0
CF7:
- Add wpcf7_spam filter registered before is_admin() early-return so
CF7 AJAX submissions (admin-ajax.php) are properly validated
- Exclude CF7 posts from generic catch-all (prevent double-checking)
Auto-flush:
- Add maybe_flush_overdue() with 5-min transient lock, hooked to
shutdown action so every PHP request can trigger a flush if overdue
- No longer depends solely on WP-Cron firing
Dashboard layout:
- Top Attackers moved into right column below live feed
- Viewport-fill layout: body/main use flex+overflow:hidden so content
stays in view; left col scrolls independently if needed
- Feed panel takes flex:1, attackers panel capped at 260px
Colors:
- --dim: #006600 → #44bb77 (legible secondary text, ~5:1 contrast)
- --dim2: #228844 added for slightly darker secondary use
- --muted kept dark for backgrounds only; border lightened slightly
IP geo (server-side, async, non-blocking):
- country + asn columns added to blocks table (migration-safe)
- enrichIP() calls ip-api.com free HTTP API per unique IP, cached 1h
- Background job enriches historic rows missing country (5 per 20s)
- Stats and live feed now include country code + ASN
- Dashboard shows country flag emoji in feed rows and attackers table
- Full AS name shown as tooltip on ASN column
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 07:57:16 +01:00
|
|
|
|
return ids;
|
feat: centralized API dashboard + Docker container
API server (api/):
- Node.js + Express + SQLite (better-sqlite3, WAL mode)
- POST /api/v1/submit — receive blocks from WP sites (rate limited 30/min/IP)
- GET /api/v1/stats — public aggregated stats with 30s cache
- GET /api/v1/stream — SSE live feed, pushed every 2s
- GET /api/v1/health — health check
- IP masking: only first 2 octets stored (192.168.x.x)
- UA family detection: curl, Python, Go, bots, Chrome, etc.
- docker-compose.yml with named volume for SQLite persistence
Dashboard (api/public/index.html):
- Hacker/terminal aesthetic: black + matrix green, CRT scanlines
- Live stat cards: total blocked, today, 7d, 30d, sites reporting
- Canvas 24h activity trend chart with gradient bars
- CSS bar charts: form types, bot toolkit, block reasons
- Live SSE threat feed with countUp animation and auto-scroll
- Top 10 attackers table with frequency bars
- Polls /api/v1/stats every 6s, SSE for instant feed updates
WordPress plugin (honeypot-fields.php):
- SmartHoneypotAPIClient: queue (WP option) + WP-cron batch flush every 5min
- log_spam() now enqueues to central API after local DB write
- Admin 'Central API' tab: enable toggle, endpoint URL, sync stats, manual flush
- Cron properly registered/deregistered on activate/deactivate
2026-03-09 19:21:41 +01:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── Express routes ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
app.use(express.json({ limit: '128kb' }));
|
|
|
|
|
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
|
|
|
|
|
2026-03-09 19:26:23 +01:00
|
|
|
|
// Submit blocks from a WordPress site (token-protected)
|
|
|
|
|
|
app.post('/api/v1/submit', requireToken, (req, res) => {
|
feat: centralized API dashboard + Docker container
API server (api/):
- Node.js + Express + SQLite (better-sqlite3, WAL mode)
- POST /api/v1/submit — receive blocks from WP sites (rate limited 30/min/IP)
- GET /api/v1/stats — public aggregated stats with 30s cache
- GET /api/v1/stream — SSE live feed, pushed every 2s
- GET /api/v1/health — health check
- IP masking: only first 2 octets stored (192.168.x.x)
- UA family detection: curl, Python, Go, bots, Chrome, etc.
- docker-compose.yml with named volume for SQLite persistence
Dashboard (api/public/index.html):
- Hacker/terminal aesthetic: black + matrix green, CRT scanlines
- Live stat cards: total blocked, today, 7d, 30d, sites reporting
- Canvas 24h activity trend chart with gradient bars
- CSS bar charts: form types, bot toolkit, block reasons
- Live SSE threat feed with countUp animation and auto-scroll
- Top 10 attackers table with frequency bars
- Polls /api/v1/stats every 6s, SSE for instant feed updates
WordPress plugin (honeypot-fields.php):
- SmartHoneypotAPIClient: queue (WP option) + WP-cron batch flush every 5min
- log_spam() now enqueues to central API after local DB write
- Admin 'Central API' tab: enable toggle, endpoint URL, sync stats, manual flush
- Cron properly registered/deregistered on activate/deactivate
2026-03-09 19:21:41 +01:00
|
|
|
|
const clientIP = (req.headers['x-forwarded-for'] || '').split(',')[0].trim()
|
|
|
|
|
|
|| req.socket.remoteAddress || '';
|
|
|
|
|
|
|
|
|
|
|
|
if (!allowed(clientIP)) {
|
|
|
|
|
|
return res.status(429).json({ error: 'Rate limit exceeded' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const { site_hash, blocks } = req.body || {};
|
|
|
|
|
|
|
|
|
|
|
|
if (!site_hash || typeof site_hash !== 'string' || site_hash.length < 8) {
|
|
|
|
|
|
return res.status(400).json({ error: 'Invalid site_hash' });
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!Array.isArray(blocks) || !blocks.length || blocks.length > 50) {
|
|
|
|
|
|
return res.status(400).json({ error: 'blocks must be array of 1–50 items' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
fix: CF7 bypass, auto-flush, layout, contrast, IP geo v2.4.0
CF7:
- Add wpcf7_spam filter registered before is_admin() early-return so
CF7 AJAX submissions (admin-ajax.php) are properly validated
- Exclude CF7 posts from generic catch-all (prevent double-checking)
Auto-flush:
- Add maybe_flush_overdue() with 5-min transient lock, hooked to
shutdown action so every PHP request can trigger a flush if overdue
- No longer depends solely on WP-Cron firing
Dashboard layout:
- Top Attackers moved into right column below live feed
- Viewport-fill layout: body/main use flex+overflow:hidden so content
stays in view; left col scrolls independently if needed
- Feed panel takes flex:1, attackers panel capped at 260px
Colors:
- --dim: #006600 → #44bb77 (legible secondary text, ~5:1 contrast)
- --dim2: #228844 added for slightly darker secondary use
- --muted kept dark for backgrounds only; border lightened slightly
IP geo (server-side, async, non-blocking):
- country + asn columns added to blocks table (migration-safe)
- enrichIP() calls ip-api.com free HTTP API per unique IP, cached 1h
- Background job enriches historic rows missing country (5 per 20s)
- Stats and live feed now include country code + ASN
- Dashboard shows country flag emoji in feed rows and attackers table
- Full AS name shown as tooltip on ASN column
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 07:57:16 +01:00
|
|
|
|
const ids = insertBatch(site_hash.slice(0, 20), blocks);
|
feat: centralized API dashboard + Docker container
API server (api/):
- Node.js + Express + SQLite (better-sqlite3, WAL mode)
- POST /api/v1/submit — receive blocks from WP sites (rate limited 30/min/IP)
- GET /api/v1/stats — public aggregated stats with 30s cache
- GET /api/v1/stream — SSE live feed, pushed every 2s
- GET /api/v1/health — health check
- IP masking: only first 2 octets stored (192.168.x.x)
- UA family detection: curl, Python, Go, bots, Chrome, etc.
- docker-compose.yml with named volume for SQLite persistence
Dashboard (api/public/index.html):
- Hacker/terminal aesthetic: black + matrix green, CRT scanlines
- Live stat cards: total blocked, today, 7d, 30d, sites reporting
- Canvas 24h activity trend chart with gradient bars
- CSS bar charts: form types, bot toolkit, block reasons
- Live SSE threat feed with countUp animation and auto-scroll
- Top 10 attackers table with frequency bars
- Polls /api/v1/stats every 6s, SSE for instant feed updates
WordPress plugin (honeypot-fields.php):
- SmartHoneypotAPIClient: queue (WP option) + WP-cron batch flush every 5min
- log_spam() now enqueues to central API after local DB write
- Admin 'Central API' tab: enable toggle, endpoint URL, sync stats, manual flush
- Cron properly registered/deregistered on activate/deactivate
2026-03-09 19:21:41 +01:00
|
|
|
|
_cache = null; // invalidate stats cache
|
fix: CF7 bypass, auto-flush, layout, contrast, IP geo v2.4.0
CF7:
- Add wpcf7_spam filter registered before is_admin() early-return so
CF7 AJAX submissions (admin-ajax.php) are properly validated
- Exclude CF7 posts from generic catch-all (prevent double-checking)
Auto-flush:
- Add maybe_flush_overdue() with 5-min transient lock, hooked to
shutdown action so every PHP request can trigger a flush if overdue
- No longer depends solely on WP-Cron firing
Dashboard layout:
- Top Attackers moved into right column below live feed
- Viewport-fill layout: body/main use flex+overflow:hidden so content
stays in view; left col scrolls independently if needed
- Feed panel takes flex:1, attackers panel capped at 260px
Colors:
- --dim: #006600 → #44bb77 (legible secondary text, ~5:1 contrast)
- --dim2: #228844 added for slightly darker secondary use
- --muted kept dark for backgrounds only; border lightened slightly
IP geo (server-side, async, non-blocking):
- country + asn columns added to blocks table (migration-safe)
- enrichIP() calls ip-api.com free HTTP API per unique IP, cached 1h
- Background job enriches historic rows missing country (5 per 20s)
- Stats and live feed now include country code + ASN
- Dashboard shows country flag emoji in feed rows and attackers table
- Full AS name shown as tooltip on ASN column
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 07:57:16 +01:00
|
|
|
|
// Enrich IPs asynchronously without blocking the response
|
|
|
|
|
|
setImmediate(() => ids.forEach(({ id, ip }) => enrichIP(id, ip)));
|
feat: centralized API dashboard + Docker container
API server (api/):
- Node.js + Express + SQLite (better-sqlite3, WAL mode)
- POST /api/v1/submit — receive blocks from WP sites (rate limited 30/min/IP)
- GET /api/v1/stats — public aggregated stats with 30s cache
- GET /api/v1/stream — SSE live feed, pushed every 2s
- GET /api/v1/health — health check
- IP masking: only first 2 octets stored (192.168.x.x)
- UA family detection: curl, Python, Go, bots, Chrome, etc.
- docker-compose.yml with named volume for SQLite persistence
Dashboard (api/public/index.html):
- Hacker/terminal aesthetic: black + matrix green, CRT scanlines
- Live stat cards: total blocked, today, 7d, 30d, sites reporting
- Canvas 24h activity trend chart with gradient bars
- CSS bar charts: form types, bot toolkit, block reasons
- Live SSE threat feed with countUp animation and auto-scroll
- Top 10 attackers table with frequency bars
- Polls /api/v1/stats every 6s, SSE for instant feed updates
WordPress plugin (honeypot-fields.php):
- SmartHoneypotAPIClient: queue (WP option) + WP-cron batch flush every 5min
- log_spam() now enqueues to central API after local DB write
- Admin 'Central API' tab: enable toggle, endpoint URL, sync stats, manual flush
- Cron properly registered/deregistered on activate/deactivate
2026-03-09 19:21:41 +01:00
|
|
|
|
res.json({ ok: true, received: blocks.length });
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('[submit]', e.message);
|
|
|
|
|
|
res.status(500).json({ error: 'Internal error' });
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Public aggregated stats
|
|
|
|
|
|
app.get('/api/v1/stats', (_, res) => res.json(getStats()));
|
|
|
|
|
|
|
|
|
|
|
|
// SSE live feed
|
|
|
|
|
|
app.get('/api/v1/stream', (req, res) => {
|
|
|
|
|
|
res.writeHead(200, {
|
|
|
|
|
|
'Content-Type': 'text/event-stream',
|
|
|
|
|
|
'Cache-Control': 'no-cache',
|
|
|
|
|
|
'Connection': 'keep-alive',
|
|
|
|
|
|
'X-Accel-Buffering': 'no',
|
|
|
|
|
|
});
|
|
|
|
|
|
res.write(':\n\n'); // flush headers
|
|
|
|
|
|
sseClients.add(res);
|
|
|
|
|
|
req.on('close', () => sseClients.delete(res));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Health check
|
|
|
|
|
|
app.get('/api/v1/health', (_, res) =>
|
|
|
|
|
|
res.json({ ok: true, uptime: process.uptime(), sse_clients: sseClients.size })
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
app.listen(PORT, '0.0.0.0', () => {
|
|
|
|
|
|
console.log(`[honeypot-api] listening on :${PORT}`);
|
|
|
|
|
|
console.log(`[honeypot-api] db: ${process.env.DB_PATH || '/data/honeypot.db'}`);
|
|
|
|
|
|
});
|