diff --git a/jpos/src/main/java/org/jpos/util/JsonlLogWriter.java b/jpos/src/main/java/org/jpos/util/JsonlLogWriter.java index aaed798f0a..78d54d6eec 100644 --- a/jpos/src/main/java/org/jpos/util/JsonlLogWriter.java +++ b/jpos/src/main/java/org/jpos/util/JsonlLogWriter.java @@ -26,6 +26,7 @@ import org.jpos.core.Configurable; import org.jpos.core.Configuration; import org.jpos.core.ConfigurationException; +import org.jpos.iso.ISOException; import org.jpos.iso.ISOMsg; import org.jpos.iso.ISOUtil; import org.jpos.log.AuditLogEvent; @@ -50,8 +51,8 @@ * *

Configuration properties (same convention as {@link ProtectedLogListener}):

* * *

Output is suitable for {@code jq}, Filebeat, and Elasticsearch ingestion.

@@ -66,8 +67,8 @@ public class JsonlLogWriter extends BaseLogEventWriter implements Configurable { private ObjectMapper mapper; private String host; - private Set protectFields = Set.of(2); - private Set wipeFields = Set.of(35, 45, 48, 52, 55); + private Set protectFields = Set.of("2"); + private Set wipeFields = Set.of("35", "45", "48", "52", "55"); /** Default constructor. */ public JsonlLogWriter() { @@ -83,11 +84,11 @@ public JsonlLogWriter() { public void setConfiguration(Configuration cfg) throws ConfigurationException { String protect = cfg.get("protect", null); if (protect != null) { - protectFields = toIntSet(protect); + protectFields = toFieldPathSet(protect); } String wipe = cfg.get("wipe", null); if (wipe != null) { - wipeFields = toIntSet(wipe); + wipeFields = toFieldPathSet(wipe); } initMapper(); } @@ -144,13 +145,10 @@ private Map buildTags(LogEvent ev) { private String protectAndDump(ISOMsg original) { ISOMsg m = (ISOMsg) original.clone(); - for (int field : protectFields) { - String v = m.getString(field); - if (v != null) { - m.set(field, ISOUtil.protect(v)); - } + for (String field : protectFields) { + protectField(m, field); } - for (int field : wipeFields) { + for (String field : wipeFields) { if (m.hasField(field)) { m.set(field, WIPED); } @@ -168,6 +166,19 @@ private String dump(Object obj) { return obj.toString(); } + private void protectField(ISOMsg m, String field) { + try { + Object v = m.getValue(field); + if (v instanceof String s) { + m.set(field, ISOUtil.protect(s)); + } else if (v != null) { + m.set(field, WIPED); + } + } catch (ISOException ignored) { + // Match ProtectedLogListener behavior: invalid or absent paths are ignored. + } + } + private void initMapper() { mapper = new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); @@ -179,13 +190,27 @@ private void initMapper() { AuditLogEventRegistry.register(mapper); } - private static Set toIntSet(String spaceSeparated) { + private static Set toFieldPathSet(String spaceSeparated) throws ConfigurationException { if (spaceSeparated == null || spaceSeparated.isBlank()) return Set.of(); - Set result = new HashSet<>(); + Set result = new HashSet<>(); for (String token : spaceSeparated.trim().split("\\s+")) { - result.add(Integer.parseInt(token)); + validateFieldPath(token); + result.add(token); } return Collections.unmodifiableSet(result); } + + private static void validateFieldPath(String fieldPath) throws ConfigurationException { + for (String token : fieldPath.split("\\.", -1)) { + if (token.isEmpty()) { + throw new ConfigurationException("Invalid ISO field path: " + fieldPath); + } + try { + Integer.parseInt(token); + } catch (NumberFormatException e) { + throw new ConfigurationException("Invalid ISO field path: " + fieldPath, e); + } + } + } } diff --git a/jpos/src/test/java/org/jpos/util/JsonlLogWriterTest.java b/jpos/src/test/java/org/jpos/util/JsonlLogWriterTest.java index 42d780d5bf..9dd7cea9da 100644 --- a/jpos/src/test/java/org/jpos/util/JsonlLogWriterTest.java +++ b/jpos/src/test/java/org/jpos/util/JsonlLogWriterTest.java @@ -142,6 +142,47 @@ void customProtectAndWipeConfig() throws Exception { assertFalse(output.contains("4111111111111111"), "PAN must not appear"); } + @Test + void protectsNestedFieldPath() throws Exception { + Properties props = new Properties(); + props.setProperty("protect", "123.1"); + props.setProperty("wipe", ""); + writer.setConfiguration(new SimpleConfiguration(props)); + + ISOMsg m = new ISOMsg(); + m.setMTI("0200"); + m.set("123.1", "4111111111111111"); + + LogEvent ev = new LogEvent("send"); + ev.addMessage(m); + writer.write(ev); + + String output = baos.toString().trim(); + assertFalse(output.contains("4111111111111111"), "nested PAN must not appear in cleartext"); + assertTrue(output.contains("411111"), "nested BIN should be preserved"); + assertTrue(output.contains("1111"), "nested last 4 should be preserved"); + } + + @Test + void wipesNestedFieldPath() throws Exception { + Properties props = new Properties(); + props.setProperty("protect", ""); + props.setProperty("wipe", "112.3.1"); + writer.setConfiguration(new SimpleConfiguration(props)); + + ISOMsg m = new ISOMsg(); + m.setMTI("0200"); + m.set("112.3.1", "secret nested value"); + + LogEvent ev = new LogEvent("send"); + ev.addMessage(m); + writer.write(ev); + + String output = baos.toString().trim(); + assertFalse(output.contains("secret nested value"), "nested field must be wiped"); + assertTrue(output.contains("[WIPED]"), "nested wiped fields should show [WIPED]"); + } + @Test void includesRealmInTags() throws Exception { Log source = new Log();