Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 40 additions & 15 deletions jpos/src/main/java/org/jpos/util/JsonlLogWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -50,8 +51,8 @@
*
* <p>Configuration properties (same convention as {@link ProtectedLogListener}):</p>
* <ul>
* <li>{@code protect} — space-separated field numbers to mask via {@link ISOUtil#protect(String)} (default: {@code "2"})</li>
* <li>{@code wipe} — space-separated field numbers to replace with [WIPED] (default: {@code "35 45 48 52 55"})</li>
* <li>{@code protect} — space-separated field paths to mask via {@link ISOUtil#protect(String)} (default: {@code "2"})</li>
* <li>{@code wipe} — space-separated field paths to replace with [WIPED] (default: {@code "35 45 48 52 55"})</li>
* </ul>
*
* <p>Output is suitable for {@code jq}, Filebeat, and Elasticsearch ingestion.</p>
Expand All @@ -66,8 +67,8 @@ public class JsonlLogWriter extends BaseLogEventWriter implements Configurable {

private ObjectMapper mapper;
private String host;
private Set<Integer> protectFields = Set.of(2);
private Set<Integer> wipeFields = Set.of(35, 45, 48, 52, 55);
private Set<String> protectFields = Set.of("2");
private Set<String> wipeFields = Set.of("35", "45", "48", "52", "55");

/** Default constructor. */
public JsonlLogWriter() {
Expand All @@ -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();
}
Expand Down Expand Up @@ -144,13 +145,10 @@ private Map<String,String> 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);
}
Expand All @@ -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());
Expand All @@ -179,13 +190,27 @@ private void initMapper() {
AuditLogEventRegistry.register(mapper);
}

private static Set<Integer> toIntSet(String spaceSeparated) {
private static Set<String> toFieldPathSet(String spaceSeparated) throws ConfigurationException {
if (spaceSeparated == null || spaceSeparated.isBlank())
return Set.of();
Set<Integer> result = new HashSet<>();
Set<String> 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);
}
}
}
}
41 changes: 41 additions & 0 deletions jpos/src/test/java/org/jpos/util/JsonlLogWriterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down