Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

fix: implement client-side filtering #174

Merged
merged 2 commits into from
Jan 18, 2024
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
7 changes: 5 additions & 2 deletions 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 */
/* Copyright (C) Red Hat 2022-2024 */
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
@@ -1,4 +1,4 @@
/* Copyright (C) Red Hat 2023 */
/* Copyright (C) Red Hat 2023-2024 */
package com.redhat.insights.reports;

import com.fasterxml.jackson.databind.JsonSerializer;
Expand Down 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-2024 */
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]);
}
}
105 changes: 104 additions & 1 deletion api/src/test/java/com/redhat/insights/TestTopLevelReport.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
/* Copyright (C) Red Hat 2023 */
/* Copyright (C) Red Hat 2023-2024 */
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.");
}
}
}