Skip to content

Commit

Permalink
feat: implement client-side filtering
Browse files Browse the repository at this point in the history
  • Loading branch information
kittylyst committed Jan 18, 2024
1 parent e53f7b9 commit ac1bc0d
Show file tree
Hide file tree
Showing 4 changed files with 233 additions and 2 deletions.
5 changes: 4 additions & 1 deletion api/src/main/java/com/redhat/insights/Filtering.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
/* Copyright (C) Red Hat 2022-2023 */
package com.redhat.insights;

import com.redhat.insights.reports.Utils;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

/** Insights data filtering function. */
public enum Filtering implements Function<Map<String, Object>, Map<String, Object>> {
DEFAULT(Function.identity()),
DEFAULT(Utils::defaultMasking),

CLEARTEXT(Function.identity()),

NOTHING(__ -> new HashMap<>());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public JsonSerializer<InsightsReport> getSerializer() {
}

@Override
public void generateReport(Filtering masking) {
public void generateReport(Filtering __) {
if (!updatedJars.isEmpty()) {
List<JarInfo> jars = new ArrayList<>();
int sendCount = updatedJars.drainTo(jars);
Expand Down
125 changes: 125 additions & 0 deletions api/src/main/java/com/redhat/insights/reports/Utils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/* Copyright (C) Red Hat 2023 */
package com.redhat.insights.reports;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public final class Utils {
private Utils() {}

@SuppressWarnings("unchecked")
public static Map<String, Object> defaultMasking(final Map<String, Object> inArgs) {
List<String> jvmArgs = new ArrayList<>();
for (String arg : (List<String>) (inArgs.get("jvm.args"))) {
jvmArgs.add(sanitizeJavaParameters(arg));
}
inArgs.put("jvm.args", jvmArgs);
inArgs.put("java.command", sanitizeJavaParameters((String) inArgs.get("java.command")));
return inArgs;
}

private static String REDACTED_VALUE = "=ZZZZZZZZZ";

/**
* Sanitizes a string that contains java style parameters of the type -Dxxxxx=yyyyy by
* substituting the yyyyy value for an obfuscated string
*
* @param parameters
* @return a sanitized parameter string suitable for persisting
*/
static String sanitizeJavaParameters(final String parameters) {
final StringBuilder out = new StringBuilder();

for (final String token : tokenizeComplexJavaParameters(parameters)) {
// We only care about -Dxxxxx=yyyyy params
if (token.startsWith("-D") && token.contains("=")) {
String[] parts = token.split("=", 2);
out.append(parts[0]);
out.append(REDACTED_VALUE);
// We might be parsing json
// if so, preserve the list comma or list closing bracket
if (token.endsWith(",")) {
out.append(',');
}
if (token.endsWith("]")) {
out.append(']');
}
} else {
out.append(token);
}
out.append(" ");
}
// Remove the last added space
out.deleteCharAt(out.length() - 1);
return out.toString();
}

// This tokenizes a string, but with some special rules
// It tokenizes based on spaces, but it will interpret quotes
// that start in the middle of a string, after an '='
// This is important because some of the data we want to preserve might
// look like -Dxxxxx="this is all one token"
// This is also aware of escape sequences
static String[] tokenizeComplexJavaParameters(final String parameters) {
final ArrayList<String> tokens = new ArrayList<String>();
StringBuilder currentWord = new StringBuilder();
Character currentQuote = null;
boolean escaping = false;
boolean afterEquals = false;
// Order is important here. Rearrange at your own risk.
for (final char c : parameters.toCharArray()) {
// If we're not escaping, start escaping and continue
if (c == '\\' && !escaping) {
escaping = true;
currentWord.append(c);
continue;
}

// If we're escaping, always just add to the word and continue
if (escaping) {
escaping = false;
currentWord.append(c);
continue;
}

// If we see an '=', remember that and continue
if (c == '=') {
afterEquals = true;
currentWord.append(c);
continue;
}

// If we're not in a quote and we hit a space, save the word and continue
if (currentQuote == null && c == ' ') {
tokens.add(currentWord.toString());
currentWord = new StringBuilder();
continue;
}

// If we see a quote...
if (c == '\'' || c == '"') {
// If we are quoting...
if (currentQuote != null) {
// stop quoting if we're at the matching quote
if (c == currentQuote) {
currentQuote = null;
}
} else {
// So we're not quoting...
// If we're at a new word or after an equals, start quoting
if (afterEquals || currentWord.length() == 0) {
currentQuote = c;
}
}
}

// Otherwise, just add the char
afterEquals = false;
currentWord.append(c);
}
// Add the last word for the end of string
tokens.add(currentWord.toString());
return tokens.toArray(new String[0]);
}
}
103 changes: 103 additions & 0 deletions api/src/test/java/com/redhat/insights/TestTopLevelReport.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
package com.redhat.insights;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.RETURNS_DEFAULTS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;

import com.redhat.insights.doubles.DummyTopLevelReport;
import com.redhat.insights.jars.ClasspathJarInfoSubreport;
Expand All @@ -10,8 +15,10 @@
import com.redhat.insights.reports.InsightsReport;
import com.redhat.insights.reports.InsightsSubreport;
import java.io.IOException;
import java.lang.management.*;
import java.util.*;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;

public class TestTopLevelReport extends AbstractReportTest {
@Test
Expand Down Expand Up @@ -243,4 +250,100 @@ public void testGenerateReportWithPackages() throws IOException {
}
}
}

@Test
public void testReportSanitization() throws IOException {
DummyTopLevelReport insightsReport = new DummyTopLevelReport(logger, Collections.emptyMap());
insightsReport.setPackages(Package.getPackages());

List<String> unsanitizedJvmArgs =
Arrays.asList(
"-D[Standalone]",
"-verbose:gc",
"-Xloggc:/opt/jboss-eap-7.4.0/standalone/log/gc.log",
"-XX:+PrintGCDetails",
"-XX:+PrintGCDateStamps",
"-XX:+UseGCLogFileRotation",
"-XX:NumberOfGCLogFiles=5",
"-XX:GCLogFileSize=3M",
"-XX:-TraceClassUnloading",
"-Djdk.serialFilter=maxbytes=10485760;maxdepth=128;maxarray=100000;maxrefs=300000",
"-Xms1303m",
"-Xmx2048m",
"-XX:MetaspaceSize=128M",
"-XX:MaxMetaspaceSize=512m",
"-Djava.net.preferIPv4Stack=true",
"-Djboss.modules.system.pkgs=org.jboss.byteman",
"-Djava.awt.headless=true",
"-Dorg.jboss.boot.log.file=/opt/jboss-eap-7.4.0/standalone/log/server.log",
"-Dsome.dumb.practice=\"Man I hope \\\"' this = works\"",
"-Dlogging.configuration=file:/opt/jboss-eap-7.4.0/standalone/configuration/logging.properties");
List<String> sanitizedJvmArgs =
Arrays.asList(
"-D[Standalone]",
"-verbose:gc",
"-Xloggc:/opt/jboss-eap-7.4.0/standalone/log/gc.log",
"-XX:+PrintGCDetails",
"-XX:+PrintGCDateStamps",
"-XX:+UseGCLogFileRotation",
"-XX:NumberOfGCLogFiles=5",
"-XX:GCLogFileSize=3M",
"-XX:-TraceClassUnloading",
"-Djdk.serialFilter=ZZZZZZZZZ",
"-Xms1303m",
"-Xmx2048m",
"-XX:MetaspaceSize=128M",
"-XX:MaxMetaspaceSize=512m",
"-Djava.net.preferIPv4Stack=ZZZZZZZZZ",
"-Djboss.modules.system.pkgs=ZZZZZZZZZ",
"-Djava.awt.headless=ZZZZZZZZZ",
"-Dorg.jboss.boot.log.file=ZZZZZZZZZ",
"-Dsome.dumb.practice=ZZZZZZZZZ",
"-Dlogging.configuration=ZZZZZZZZZ");

// Mock the ManagementFactory and RuntimeMXBean to make it give our data
// But first collect the necessary beans to give back to the ManagementFactory
// If you don't those methods will return null, even if you use
// CallRealMethod or RETURNS_DEFAULTS
OperatingSystemMXBean systemMXBean = ManagementFactory.getOperatingSystemMXBean();
MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
List<GarbageCollectorMXBean> gcMxBeans = ManagementFactory.getGarbageCollectorMXBeans();
try (MockedStatic<ManagementFactory> mockFactory =
mockStatic(ManagementFactory.class, withSettings().defaultAnswer(RETURNS_DEFAULTS))) {
RuntimeMXBean mockRuntimeBean =
mock(RuntimeMXBean.class, withSettings().defaultAnswer(RETURNS_DEFAULTS));
mockFactory.when(ManagementFactory::getOperatingSystemMXBean).thenReturn(systemMXBean);
mockFactory.when(ManagementFactory::getMemoryMXBean).thenReturn(memoryMXBean);
mockFactory.when(ManagementFactory::getGarbageCollectorMXBeans).thenReturn(gcMxBeans);
when(mockRuntimeBean.getInputArguments()).thenReturn(unsanitizedJvmArgs);
mockFactory.when(ManagementFactory::getRuntimeMXBean).thenReturn(mockRuntimeBean);

String unsanitizedJavaCommand =
"/opt/jboss/7/eap/jboss-modules.jar -mp"
+ " /opt/jboss/7/eap/modules:/opt/jboss/7/eap/../modules org.jboss.as.standalone"
+ " -Djboss.home.dir=/opt/jboss/7/eap"
+ " -Djboss.server.base.dir=/opt/jboss/7/instances/jboss-bdi-dwhprosa -c"
+ " standalone.xml -Djboss.server.base.dir=/opt/jboss/7/instances/jboss-bdi-dwhprosa";
String sanitizedJavaCommand =
"/opt/jboss/7/eap/jboss-modules.jar -mp"
+ " /opt/jboss/7/eap/modules:/opt/jboss/7/eap/../modules org.jboss.as.standalone"
+ " -Djboss.home.dir=ZZZZZZZZZ -Djboss.server.base.dir=ZZZZZZZZZ -c standalone.xml"
+ " -Djboss.server.base.dir=ZZZZZZZZZ";

// Set our java command property
System.setProperty("sun.java.command", unsanitizedJavaCommand);

String report = generateReport(insightsReport);
Map<?, ?> basicReport = (Map<?, ?>) parseReport(report).get("basic");

assertEquals(
sanitizedJvmArgs,
basicReport.get("jvm.args"),
"The \"jvm.args\" property in the basic report should be properly sanitized.");
assertEquals(
sanitizedJavaCommand,
basicReport.get("java.command"),
"The \"java.command\" property in the basic report should be properly sanitized.");
}
}
}

0 comments on commit ac1bc0d

Please sign in to comment.