From 19cdf8dc9f90edb53653e358b64bfb3dbbda114a Mon Sep 17 00:00:00 2001 From: Kevin Nguyen Date: Wed, 29 Jul 2026 22:32:18 +0700 Subject: [PATCH] CB-505 fix: audit lines were not valid JSON MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first cut spliced the timestamp on via a logback pattern: {"ts":"%d{...}",%replace(%msg){'^\{',''}%n Logback's variable substitution chokes on the literal braces ("All tokens consumed but was expecting }"), so the encoder failed to configure. Caught by running the real jar and noticing logback had dumped its internal status — which it only does when something failed to parse. The build was green throughout: nothing asserted the audit trail was machine-readable. AuditLog now emits the complete object including its own ISO-8601 "ts", and the appender pattern is a bare %msg. Adds AuditLogTest, which parses each emitted line with Jackson (so a malformed record fails the build) and pins that hostile ids cannot escape their field to forge a second record. 311 tests green; logback now configures with zero internal errors. --- .../java/dev/ltms/bridged/auth/AuditLog.java | 13 ++- bridged/src/main/resources/logback.xml | 8 +- .../dev/ltms/bridged/auth/AuditLogTest.java | 100 ++++++++++++++++++ 3 files changed, 115 insertions(+), 6 deletions(-) create mode 100644 bridged/src/test/java/dev/ltms/bridged/auth/AuditLogTest.java diff --git a/bridged/src/main/java/dev/ltms/bridged/auth/AuditLog.java b/bridged/src/main/java/dev/ltms/bridged/auth/AuditLog.java index e564466..1554dbd 100644 --- a/bridged/src/main/java/dev/ltms/bridged/auth/AuditLog.java +++ b/bridged/src/main/java/dev/ltms/bridged/auth/AuditLog.java @@ -3,6 +3,10 @@ package dev.ltms.bridged.auth; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; + /** * Append-only record of privileged actions (CB-505). * @@ -18,6 +22,8 @@ import org.slf4j.LoggerFactory; public final class AuditLog { private static final Logger AUDIT = LoggerFactory.getLogger("audit"); + private static final DateTimeFormatter TS = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX").withZone(ZoneOffset.UTC); private AuditLog() { } @@ -40,8 +46,11 @@ public final class AuditLog { private static void write(Principal caller, Authz.Action action, String target, String outcome, String reason) { Principal c = caller != null ? caller : Principal.anonymous(); - StringBuilder sb = new StringBuilder(160); - sb.append("{\"role\":\"").append(c.role()).append('"') + StringBuilder sb = new StringBuilder(200); + // The timestamp is built here rather than by the appender pattern: a pattern that wrapped + // literal braces around the message collides with logback's own variable substitution. + sb.append("{\"ts\":\"").append(TS.format(Instant.now())).append('"') + .append(",\"role\":\"").append(c.role()).append('"') .append(",\"actor\":\"").append(esc(c.describe())).append('"') .append(",\"pid\":").append(c.pid()) .append(",\"action\":\"").append(action).append('"') diff --git a/bridged/src/main/resources/logback.xml b/bridged/src/main/resources/logback.xml index 1407ceb..afa8ab5 100644 --- a/bridged/src/main/resources/logback.xml +++ b/bridged/src/main/resources/logback.xml @@ -8,9 +8,9 @@