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}):
*
- * - {@code protect} — space-separated field numbers to mask via {@link ISOUtil#protect(String)} (default: {@code "2"})
- * - {@code wipe} — space-separated field numbers to replace with [WIPED] (default: {@code "35 45 48 52 55"})
+ * - {@code protect} — space-separated field paths to mask via {@link ISOUtil#protect(String)} (default: {@code "2"})
+ * - {@code wipe} — space-separated field paths to replace with [WIPED] (default: {@code "35 45 48 52 55"})
*
*
* 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();