Add Security Center — multi-source event pipeline with dashboard UI
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled

Introduces a unified security event store and HTTP API that ingests events
from any of the configured sources (API audit, Caddy access log, fail2ban,
shared_bans, future remote agents) and surfaces them in the dashboard.

New files:
  src/security/event-store.js      JSONL-backed store + in-memory query index
  src/security/host-registry.js    Registered hosts with per-host API keys
  src/security/event-workers.js    Tail-followers for Caddy/fail2ban/shared_bans logs
  routes/security.js               Events, hosts, ingest, SSE stream endpoints
  status/js/security-center.js     Dashboard modal with Overview/Events/Hosts tabs
  SECURITY-FEATURE.md              Full feature documentation
  DEAD-CODE.md, DUP-CODE.md, HARDENING.md   Prior audits

Modified:
  src/app.js                       Mount /api/v1/security/*
  src/utilities/middleware.js      Add ingest endpoints to PUBLIC_ROUTES
  src/security/audit-logger.js     Mirror audit events into security store
  server.js                        Start security workers on boot
  status/build.js                  Bundle security-center.js
  status/index.html                Add Security button to nav
This commit is contained in:
hermes
2026-07-13 02:28:56 -07:00
parent f405186eb8
commit c9d067c2f0
15 changed files with 2387 additions and 0 deletions
@@ -119,6 +119,25 @@ class AuditLogger {
return false;
}
/**
* Map a (action, outcome) pair to a severity level for the security event store.
* Most actions are 'info', but security-sensitive ones get escalated.
*/
resolveSeverity(action, outcome) {
// Failed auth + sensitive actions are warnings at minimum
if (outcome === 'failure' || outcome === 'denied' || outcome === 'error') {
if (action?.startsWith('auth.')) return 'warn';
if (action?.includes('credential')) return 'warn';
if (action?.includes('delete') || action?.includes('disable')) return 'warn';
return 'notice';
}
// Successful sensitive actions (key generation, TOTP setup, config changes)
if (action?.startsWith('auth.totp-') || action?.includes('rotate-key')) return 'notice';
if (action?.includes('delete') || action?.includes('disable')) return 'notice';
if (action?.startsWith('config.')) return 'notice';
return 'info';
}
async log({ action, resource, details, outcome, ip }) {
try {
const entry = {
@@ -138,6 +157,34 @@ class AuditLogger {
}
return entries;
});
// ALSO emit to the unified security event store so security events from
// the API show up alongside Caddy access logs, fail2ban events, and any
// future remote-agent events in one timeline. This is best-effort —
// failure here MUST NOT block the audit log write.
try {
const { getStore } = require('./event-store');
const store = getStore();
const severity = this.resolveSeverity(action, outcome);
const hostname = require('os').hostname();
store.append({
source_host: hostname,
source_type: 'api',
actor: ip || null,
target: resource || null,
action: action || 'unknown',
outcome: outcome || 'unknown',
severity,
message: `${action} ${outcome} on ${resource}`.trim(),
metadata: {
method: details?.body && Object.keys(details.body)[0] ? '(see audit-log)' : undefined,
audit_id: entry.id,
},
});
} catch (e) {
// Non-fatal — security store is a best-effort mirror
console.error('[AuditLogger] Security event emit failed:', e.message);
}
} catch (e) {
console.error('[AuditLogger] Failed to write entry:', e.message);
}