Add Bot Activity admin panel: banned-IP view + recent events + emergency unban

Expose the botScore middleware's in-memory scoring/ban state to admins.
Previously state lived only in the store Map with no persistence or API — the
only visibility was tailing container logs.

- botScore: bounded ring buffer (300) recording scan/login-fail/honeypot and
  ban events (most-recent-first); listState() snapshot of all scored IPs;
  unban() to clear a single IP.
- New admin-only endpoints GET /admin/bot-activity and
  POST /admin/bot-activity/unban (RBAC admin gate, IP validated). Unban is
  activity-logged with the admin username.
- Bot Activity tab: currently-banned table with Unban, plus a recent-events
  feed, following the existing admin table patterns.
- Tests for the buffer, listState, and unban (guard lets an unbanned IP back
  through). README updated.

Read + emergency-unban only — no ban-add or weight-editing surface. Buffer is
in-memory, matching the store; not persisted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 02:31:25 -05:00
parent 58852a5078
commit 870971fc12
9 changed files with 341 additions and 6 deletions

View File

@@ -0,0 +1,31 @@
// Bot-scoring / IP-ban visibility for admins. Read-only view of the botScore
// middleware's in-memory state plus a recent-events feed, and a single mutating
// action — an emergency unban for false positives. Mounted behind the admin-only
// RBAC gate (see admin.routes.js). This is visibility + emergency unban only;
// there is deliberately no way to add a ban or change scoring weights from here.
const botScore = require('../../../middleware/botScore')
const activity = require('../../../model/activity/activity.model')
const log = require('../../../utils/logger')('botactivity')
// Current store state (all scored IPs, banned or not) plus the recent-events
// buffer, most-recent-first. Both are in-memory and reset on process restart.
async function getBotActivity(req, res) {
return res.json({
ips: botScore.listState(),
events: botScore.recentEvents(),
})
}
// Emergency unban: clear a single IP's entry so it is no longer banned or
// carrying score. A real administrative action — logged with the admin user.
async function unbanIp(req, res) {
const ip = req.body.ip
const removed = botScore.unban(ip)
await activity.log({ req, action: 'botscore.unban', detail: { ip, removed } })
log.info('IP unbanned by admin', { ip, admin: req.user.username, removed })
return res.json({ ip, removed })
}
module.exports = { getBotActivity, unbanIp }