fix(security): audit-logger PII masking parity with unified logger (DC-110) [glm-grade=A]
CI / Test & Lint (push) Canceled after 0s
CI / Security audit (push) Canceled after 0s

audit-logger.js (StateManager write path into audit-log.json) starred only
6 sensitive keys at the middleware layer; email-bearing resource paths
(/invites/<email>/accept), DC-048 details.userEmail attribution, and emails
in non-sensitive body keys landed RAW — while the parallel unified-logger
path has masked at every sink since DC-095. DC-110 closes the parity gap
using the SAME canonical primitives (sa****@example.com): log() masks
resource (maskEmailsInString) and deep-masks details (maskEmails) at the
single write-point, covering middleware AND direct route calls. The
security-event mirror now uses the masked entry.resource for target/message
(judge round-1 fix-first: the raw parameter leaked emails into
security-events.jsonl). logging.js exports maskEmails (export-only).
maskEmails clones — caller details objects are never mutated.

Judge: GLM-5.3 cold read (standing Sami authorization 2026-08-17; Codex
quota dead until 2026-08-29). Round 1 (deleg_ebb83285) C fix-first —
caught the event-store mirror leak. Round 2 (deleg_923f9076) after
in-commit fix + mirror test: grade A, ship. Verdict URN
urn:ump:mwtxoj6dbdq7bfeba3l2am2rgx34zjimcjde5f6sxz6tozsjbskq (GET
readback verified: grade A, topic codex-judge-verdict). Deferred
(judge-accepted): one-time scrub of historical raw-email lines in the
live 16MB security-events.jsonl — queued follow-up.

Tests: 123 suites / 2801 green (+6 DC-110 pins: resource+details mask,
non-mutation, middleware e2e with *** survival, idempotence, no-email
regression, masked mirror target/message).
This commit is contained in:
Hermes
2026-08-23 08:01:29 -07:00
parent 9322831f1b
commit 0721b1cb04
3 changed files with 194 additions and 4 deletions
+20 -4
View File
@@ -2,6 +2,9 @@ const path = require('path');
const StateManager = require('../managers/state-manager');
const crypto = require('crypto');
const platformPaths = require('../../platform-paths');
// DC-110: canonical email-mask primitives from the unified logger — same
// EMAIL_RE + maskEmailAddress shape every other sink uses (DC-095/DC-109).
const { maskEmails, maskEmailsInString } = require('../utils/logging');
const AUDIT_LOG_FILE = process.env.AUDIT_LOG_FILE || path.join(platformPaths.dataDir, 'audit-log.json');
const MAX_ENTRIES = parseInt(process.env.AUDIT_MAX_ENTRIES || '1000', 10);
@@ -141,13 +144,23 @@ class AuditLogger {
async log({ action, resource, details, outcome, ip }) {
try {
// DC-110: PII parity with the unified logger (DC-095). Every string
// that reaches audit-log.json gets email-masked with the SAME
// canonical mask (sa****@example.com) the other sinks use, so one
// consistent masked form everywhere. Applied HERE — the single
// write-point — instead of at each call site: covers middleware
// bodies, DC-048 userEmail attribution, direct route calls, and the
// security-event-store mirror below, regardless of caller. `action`
// is an internal token (service.create / dns.add-record) and never
// contains PII; `ip` is an address literal. maskEmails() clones, so
// the caller's `details` object is never mutated.
const entry = {
id: crypto.randomUUID(),
timestamp: new Date().toISOString(),
ip: ip || '',
action: action || '',
resource: resource || '',
details: details || {},
resource: maskEmailsInString(resource || ''),
details: maskEmails(details || {}),
outcome: outcome || 'unknown'
};
@@ -172,11 +185,14 @@ class AuditLogger {
source_host: hostname,
source_type: 'api',
actor: ip || null,
target: resource || null,
// DC-110 round 2 (judge fix-first): use the MASKED entry.resource
// — the raw parameter leaked emails into security-events.jsonl
// via target and the message template.
target: entry.resource || null,
action: action || 'unknown',
outcome: outcome || 'unknown',
severity,
message: `${action} ${outcome} on ${resource}`.trim(),
message: `${action} ${outcome} on ${entry.resource}`.trim(),
metadata: {
method: details?.body && Object.keys(details.body)[0] ? '(see audit-log)' : undefined,
audit_id: entry.id,
+1
View File
@@ -691,4 +691,5 @@ module.exports = {
EMAIL_RE,
maskEmailAddress,
maskEmailsInString,
maskEmails,
};