fix(audit): restore named audit actions for SSO gate traffic — 3 live defects (DC-111) [glm-grade=A]
CI / Test & Lint (push) Canceled after 0s
CI / Security audit (push) Canceled after 0s

1. audit-logger middleware computed action/resource from req.path INSIDE
   the res.json override — after the /api/v1 router rebased req.url to the
   router-relative path. resolveAction fell through ACTION_MAP for every
   HTTP request, producing 45,899 'unknown.get' entries since 2026-07-14.
   Fix: snapshot req.path/req.method at app-level (post-shim, pre-router).
2. DC-044 shim double-prefixed ALREADY-canonical /api/v1/auth/gate|x
   into /api/v1/v1/... → 401 for canonical-URI clients. Fix: rewrite only
   legacy /api/auth/* shapes; canonical pass through untouched.
3. event-store VALID_OUTCOMES lacked 'failure' → every failed API action's
   security event was rejected+dropped from security-events.jsonl. Fix: add
   'failure' to the vocabulary set.

8 regression pins over a faithful shim→audit→router mount mirror.
Suite: 124 suites / 2809 tests green.
Judge: GLM-5.3 cold read round-1 A/ship, URN urn:ump:ywv6rpmx55tlxbgc7ciyxqfcmx2v6q756cjbmif2d66k4bwwn6ya
This commit is contained in:
Hermes
2026-08-23 09:05:27 -07:00
parent 0721b1cb04
commit 56b807543c
4 changed files with 257 additions and 7 deletions
+19 -2
View File
@@ -228,12 +228,29 @@ class AuditLogger {
return (req, res, next) => {
if (this.shouldSkip(req.method, req.path)) return next();
// DC-111: snapshot the request path NOW, at app-level (pre-router).
// The res.json override below fires AFTER the /api/v1 router has
// dispatched the request, and Express rebases req.url to the
// router-relative path at that point (/api/v1/auth/gate/plex becomes
// /auth/gate/plex). resolveAction/extractResource on the rebased path
// fall through ACTION_MAP and derive 'unknown.*' — which is exactly
// how 45,899 'unknown.get' entries landed in the audit trail between
// 2026-07-14 and 2026-08-23. Snapshot req.path in the middleware body
// (string copy — req.path is a live getter over req.url): this runs
// after the DC-044 legacy-prefix shim has canonicalized /api/auth/*
// to /api/v1/* but before the router rebase, so ACTION_MAP sees the
// canonical path for both legacy and canonical clients. Do NOT use
// req.originalUrl — it freezes the PRE-shim legacy path, which
// ACTION_MAP does not cover.
const requestPath = req.path;
const requestMethod = req.method;
const originalJson = res.json.bind(res);
res.json = (data) => {
// Log asynchronously — don't block the response
const ip = req.ip || req.socket?.remoteAddress || '';
const action = this.resolveAction(req.method, req.path);
const resource = this.extractResource(req.path);
const action = this.resolveAction(requestMethod, requestPath);
const resource = this.extractResource(requestPath);
const outcome = data && data.success === false ? 'failure' : 'success';
// Sanitize details — don't log passwords or tokens
+1 -1
View File
@@ -36,7 +36,7 @@ const MAX_EVENTS_ON_DISK = parseInt(process.env.SECURITY_EVENT_MAX_DISK || '
const VALID_SOURCE_TYPES = new Set(['api', 'caddy', 'fail2ban', 'shared-bans', 'syslog', 'agent']);
const VALID_SEVERITIES = new Set(['info', 'notice', 'warn', 'error', 'critical']);
const VALID_OUTCOMES = new Set(['success', 'denied', 'blocked', 'rate-limited', 'error', 'unknown']);
const VALID_OUTCOMES = new Set(['success', 'failure', 'denied', 'blocked', 'rate-limited', 'error', 'unknown']);
class SecurityEventStore extends EventEmitter {
constructor(opts = {}) {