-
Notifications
You must be signed in to change notification settings - Fork 154
Dlp #1909
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Dlp #1909
Changes from 14 commits
bcc7dcb
497a43c
f0119d3
5f84eb6
e2536fe
6ebb9c4
9d2e722
699b226
7ffee70
559a62b
de3ed95
4c3ff8f
8cfb161
b17b14a
099ec70
02d75ec
2be464b
450204b
75b56d9
8adedb9
e7cc895
0f673bc
6a0bed1
469256e
53bc2f8
7d04071
ff15443
ea1e179
dce7806
e2eaa48
13711b1
7aa7e95
60a13df
0bf1268
21c3489
20ed318
c20c70d
528cc14
8d75f7c
c3ec071
e9a6286
8b1a278
77022dd
5008ee7
e58e11d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| package com.predic8.membrane.core.interceptor.dlp; | ||
|
|
||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| import java.io.BufferedReader; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import java.io.InputStreamReader; | ||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
|
|
||
| public class CsvFieldConfiguration implements FieldConfiguration { | ||
|
|
||
| private static final Logger log = LoggerFactory.getLogger(CsvFieldConfiguration.class); | ||
|
|
||
| @Override | ||
| public Map<String, String> getFields(String fileName) { | ||
| try (InputStream inputStream = CsvFieldConfiguration.class.getClassLoader().getResourceAsStream(fileName)) { | ||
| Map<String, String> riskDict = new HashMap<>(); | ||
| if (inputStream == null) { | ||
| log.error("Could not find file: {}", fileName); | ||
| throw new NullPointerException("InputStream is null. File not found: " + fileName); | ||
| } | ||
|
|
||
| BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); | ||
| String line; | ||
| boolean isHeader = true; | ||
|
|
||
| while ((line = reader.readLine()) != null) { | ||
| if (isHeader) { | ||
| isHeader = false; | ||
| continue; | ||
| } | ||
|
|
||
| String[] parts = line.split(",", -1); | ||
| if (parts.length >= 3) { | ||
| String field = parts[0].trim().toLowerCase(); | ||
| String riskLevel = parts[2].trim(); | ||
| riskDict.put(field, riskLevel); | ||
| } else { | ||
| log.warn("Invalid CSV line: {}", line); | ||
| } | ||
| } | ||
| return riskDict; | ||
| } catch (IOException e) { | ||
| throw new RuntimeException("Failed to load risk data from " + fileName, e); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| package com.predic8.membrane.core.interceptor.dlp; | ||
|
|
||
| import com.fasterxml.jackson.core.JsonFactory; | ||
| import com.fasterxml.jackson.core.JsonFactoryBuilder; | ||
| import com.fasterxml.jackson.core.JsonParser; | ||
| import com.fasterxml.jackson.core.StreamReadConstraints; | ||
| import com.fasterxml.jackson.core.json.JsonReadFeature; | ||
| import com.predic8.membrane.core.http.Message; | ||
|
|
||
| import java.io.BufferedReader; | ||
| import java.io.InputStreamReader; | ||
| import java.nio.charset.Charset; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.*; | ||
|
|
||
| public class DLPAnalyzer { | ||
|
|
||
| private static final int MAX_DEPTH = 64; | ||
| private static final int MAX_STRING_LENGTH = 16 * 1024; // 16 KiB | ||
|
|
||
| private static final JsonFactory JSON_FACTORY = new JsonFactoryBuilder() | ||
| .configure(JsonReadFeature.ALLOW_TRAILING_COMMA, true) | ||
| .configure(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS, false) | ||
| .streamReadConstraints(StreamReadConstraints.builder() | ||
| .maxNestingDepth(MAX_DEPTH) | ||
| .maxStringLength(MAX_STRING_LENGTH) | ||
| .build()) | ||
| .build(); | ||
|
|
||
| private final Map<String, String> riskDict; | ||
|
|
||
| public DLPAnalyzer(Map<String, String> riskDict) { | ||
| this.riskDict = Map.copyOf(riskDict); // immutable defensive copy | ||
| } | ||
|
|
||
| public RiskReport analyze(Message msg) { | ||
| try (BufferedReader reader = new BufferedReader(new InputStreamReader(msg.getBodyAsStreamDecoded(), Optional.ofNullable(msg.getCharset()) | ||
| .map(Charset::forName) | ||
| .orElse(StandardCharsets.UTF_8))); | ||
| JsonParser parser = JSON_FACTORY.createParser(reader)) { | ||
|
|
||
| Deque<String> ctx = new ArrayDeque<>(); | ||
| RiskReport report = new RiskReport(); | ||
| String currentField = null; | ||
|
|
||
| while (parser.nextToken() != null) { | ||
| switch (parser.currentToken()) { | ||
| case FIELD_NAME -> currentField = parser.currentName(); | ||
| case START_OBJECT, START_ARRAY -> { | ||
| if (currentField != null) { | ||
| ctx.addLast(currentField); | ||
| currentField = null; | ||
| } | ||
| } | ||
| case END_OBJECT, END_ARRAY -> { | ||
| if (!ctx.isEmpty()) ctx.removeLast(); | ||
| } | ||
| default -> { | ||
| if (currentField != null) { | ||
| String fullPath = buildFullPath(ctx, currentField); | ||
| String lvl = classify(fullPath, currentField); | ||
| report.recordField(fullPath, lvl); | ||
| currentField = null; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return report; | ||
| } catch (Exception e) { | ||
| throw new RuntimeException(e); | ||
| } | ||
| } | ||
|
|
||
| private String classify(String fullPath, String simpleName) { | ||
| return Optional.ofNullable(riskDict.get(fullPath.toLowerCase())) | ||
| .or(() -> Optional.ofNullable(riskDict.get(simpleName.toLowerCase()))) | ||
| .orElse("unclassified") | ||
| .toLowerCase(); | ||
| } | ||
|
|
||
| private static String buildFullPath(Deque<String> stack, String field) { | ||
| if (stack.isEmpty()) return field; | ||
| List<String> path = new ArrayList<>(stack); | ||
| path.add(field); | ||
| return String.join(".", path); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| package com.predic8.membrane.core.interceptor.dlp; | ||
|
|
||
| import com.predic8.membrane.annot.MCAttribute; | ||
| import com.predic8.membrane.annot.MCChildElement; | ||
| import com.predic8.membrane.annot.MCElement; | ||
| import com.predic8.membrane.core.exchange.Exchange; | ||
| import com.predic8.membrane.core.http.Message; | ||
| import com.predic8.membrane.core.interceptor.AbstractInterceptor; | ||
| import com.predic8.membrane.core.interceptor.Outcome; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| import static com.predic8.membrane.core.interceptor.Outcome.CONTINUE; | ||
|
|
||
| @MCElement(name = "dlp") | ||
| public class DLPInterceptor extends AbstractInterceptor { | ||
|
|
||
| private static final Logger log = LoggerFactory.getLogger(DLPInterceptor.class); | ||
| private DLPAnalyzer dlpAnalyzer; | ||
| private String fieldsConfig; | ||
| private String action = "report"; | ||
| private Fields fields; | ||
|
|
||
| @Override | ||
| public void init() { | ||
| super.init(); | ||
| dlpAnalyzer = new DLPAnalyzer(new CsvFieldConfiguration().getFields(fieldsConfig)); | ||
| } | ||
|
|
||
| @Override | ||
| public Outcome handleRequest(Exchange exc) { | ||
| return handleInternal(exc.getRequest()); | ||
| } | ||
|
|
||
| @Override | ||
| public Outcome handleResponse(Exchange exc) { | ||
| return handleInternal(exc.getResponse()); | ||
| } | ||
|
|
||
| public Outcome handleInternal(Message msg) { | ||
| if (dlpAnalyzer == null) { | ||
| log.warn("DLP not initialized."); | ||
| return CONTINUE; | ||
| } | ||
|
|
||
| RiskReport report = dlpAnalyzer.analyze(msg); | ||
| log.info("DLP Risk Analysis: {}", report.getLogReport()); | ||
|
|
||
| if (fields != null && !fields.getFields().isEmpty()) { | ||
| for (Field f : fields.getFields()) { | ||
| f.handleAction(msg); | ||
| } | ||
| } else { | ||
| report.getMatchedFields().keySet().forEach(name -> { | ||
| Field f = new Field(); | ||
| f.setName(name); | ||
| f.setAction(action); | ||
| f.handleAction(msg); | ||
| }); | ||
| } | ||
| return CONTINUE; | ||
| } | ||
|
|
||
| public String getFieldsConfig() { | ||
| return fieldsConfig; | ||
| } | ||
|
|
||
| @MCAttribute | ||
| public void setFieldsConfig(String fieldsConfig) { | ||
| this.fieldsConfig = fieldsConfig; | ||
| } | ||
|
|
||
| public String getAction() { | ||
| return action; | ||
| } | ||
|
|
||
| @MCAttribute | ||
| public void setAction(String action) { | ||
| this.action = action; | ||
| } | ||
|
||
|
|
||
| public Fields getFields() { | ||
| return fields; | ||
| } | ||
|
|
||
| @MCChildElement | ||
| public void setFields(Fields fields) { | ||
| this.fields = fields; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,65 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| package com.predic8.membrane.core.interceptor.dlp; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| import com.fasterxml.jackson.databind.JsonNode; | ||||||||||||||||||||||||||||||||||||||||||||||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||||||||||||||||||||||||||||||||||||||||||||||
| import com.predic8.membrane.annot.MCAttribute; | ||||||||||||||||||||||||||||||||||||||||||||||
| import com.predic8.membrane.annot.MCElement; | ||||||||||||||||||||||||||||||||||||||||||||||
| import com.predic8.membrane.core.http.Message; | ||||||||||||||||||||||||||||||||||||||||||||||
| import org.slf4j.Logger; | ||||||||||||||||||||||||||||||||||||||||||||||
| import org.slf4j.LoggerFactory; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.Locale; | ||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.Objects; | ||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.regex.Pattern; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| @MCElement(name = "field") | ||||||||||||||||||||||||||||||||||||||||||||||
| public class Field { | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| private static final Logger log = LoggerFactory.getLogger(Field.class); | ||||||||||||||||||||||||||||||||||||||||||||||
| private static final ObjectMapper MAPPER = new ObjectMapper(); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| private String name; | ||||||||||||||||||||||||||||||||||||||||||||||
| private Action action = Action.REPORT; | ||||||||||||||||||||||||||||||||||||||||||||||
| private Pattern compiled = Pattern.compile(".*"); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| @MCAttribute | ||||||||||||||||||||||||||||||||||||||||||||||
| public void setName(String name) { | ||||||||||||||||||||||||||||||||||||||||||||||
| this.name = Objects.requireNonNull(name, "field name must not be null"); | ||||||||||||||||||||||||||||||||||||||||||||||
| this.compiled = Pattern.compile(name, Pattern.CASE_INSENSITIVE); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| @MCAttribute | ||||||||||||||||||||||||||||||||||||||||||||||
| public void setAction(String action) { | ||||||||||||||||||||||||||||||||||||||||||||||
| this.action = Action.valueOf(action.toUpperCase(Locale.ROOT)); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| public String getName() { | ||||||||||||||||||||||||||||||||||||||||||||||
| return name; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| public String getAction() { | ||||||||||||||||||||||||||||||||||||||||||||||
| return action.name().toLowerCase(Locale.ROOT); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| public void handleAction(Message msg) { | ||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
| JsonNode root = MAPPER.readTree(msg.getBodyAsStringDecoded()); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| switch (action) { | ||||||||||||||||||||||||||||||||||||||||||||||
| case FILTER -> JsonUtils.filter(root, compiled); | ||||||||||||||||||||||||||||||||||||||||||||||
| case MASK -> JsonUtils.mask(root, compiled); | ||||||||||||||||||||||||||||||||||||||||||||||
| case REPORT -> {/* no?op */} | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| byte[] out = MAPPER.writeValueAsBytes(root); | ||||||||||||||||||||||||||||||||||||||||||||||
| msg.setBodyContent(out); | ||||||||||||||||||||||||||||||||||||||||||||||
| msg.getHeader().setContentLength(out.length); | ||||||||||||||||||||||||||||||||||||||||||||||
| msg.getHeader().setContentType("application/json; charset=UTF-8"); | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
| switch (action) { | |
| case FILTER -> JsonUtils.filter(root, compiled); | |
| case MASK -> JsonUtils.mask(root, compiled); | |
| case REPORT -> {/* no?op */} | |
| } | |
| byte[] out = MAPPER.writeValueAsBytes(root); | |
| msg.setBodyContent(out); | |
| msg.getHeader().setContentLength(out.length); | |
| msg.getHeader().setContentType("application/json; charset=UTF-8"); | |
| switch (action) { | |
| case FILTER -> JsonUtils.filter(root, compiled); | |
| case MASK -> JsonUtils.mask(root, compiled); | |
| case REPORT -> { /* no-op */ } | |
| } | |
| if (action != Action.REPORT) { | |
| byte[] out = MAPPER.writeValueAsBytes(root); | |
| msg.setBodyContent(out); | |
| msg.getHeader().setContentLength(out.length); | |
| msg.getHeader().setContentType("application/json; charset=UTF-8"); | |
| } |
🤖 Prompt for AI Agents
In core/src/main/java/com/predic8/membrane/core/interceptor/dlp/Field.java
around lines 49 to 58, the code updates the message body and headers even when
the action is REPORT, which does nothing and causes unnecessary processing.
Modify the code to skip updating the message body and headers when the action is
REPORT, so that no changes or overhead occur for this no-op case.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.predic8.membrane.core.interceptor.dlp; | ||
|
|
||
| import java.util.Map; | ||
|
|
||
| public interface FieldConfiguration { | ||
| Map<String, String> getFields(String fileName); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package com.predic8.membrane.core.interceptor.dlp; | ||
|
|
||
| import com.predic8.membrane.annot.MCChildElement; | ||
| import com.predic8.membrane.annot.MCElement; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| @MCElement(name = "fields") | ||
| public class Fields { | ||
|
|
||
| private List<Field> fields = new ArrayList<>(); | ||
|
|
||
| @MCChildElement | ||
| public Fields setFields(List<Field> fields) { | ||
| this.fields = fields; | ||
| return this; | ||
| } | ||
|
|
||
| public List<Field> getFields() { | ||
| return fields; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| package com.predic8.membrane.core.interceptor.dlp; | ||
|
|
||
| import com.fasterxml.jackson.databind.JsonNode; | ||
| import com.fasterxml.jackson.databind.node.ObjectNode; | ||
|
|
||
| import java.util.Iterator; | ||
| import java.util.Map; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| class JsonUtils { | ||
|
|
||
| static void filter(JsonNode node, Pattern p) { | ||
| traverse(node, p, true); | ||
| } | ||
|
|
||
| static void mask(JsonNode node, Pattern p) { | ||
| traverse(node, p, false); | ||
| } | ||
|
|
||
| private static void traverse(JsonNode node, Pattern p, boolean remove) { | ||
| if (node.isObject()) { | ||
| ObjectNode obj = (ObjectNode) node; | ||
| Iterator<Map.Entry<String, JsonNode>> it = obj.fields(); | ||
| while (it.hasNext()) { | ||
| Map.Entry<String, JsonNode> e = it.next(); | ||
| String key = e.getKey(); | ||
| if (p.matcher(key).matches()) { | ||
| if (remove) { | ||
| it.remove(); | ||
| } else { | ||
| obj.put(key, "****"); | ||
| } | ||
| } else { | ||
| traverse(e.getValue(), p, remove); | ||
| } | ||
| } | ||
| } else if (node.isArray()) { | ||
| for (JsonNode child : node) { | ||
| traverse(child, p, remove); | ||
| } | ||
| } | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.