diff --git a/.gitignore b/.gitignore index 32c595d..0d7f9ec 100644 --- a/.gitignore +++ b/.gitignore @@ -221,3 +221,4 @@ _meta .serverless package-lock.json src/main/resources/lumigo-agent.jar +.vscode/ diff --git a/pom.xml b/pom.xml index ec31760..4c04a10 100644 --- a/pom.xml +++ b/pom.xml @@ -1,25 +1,19 @@ - + 4.0.0 - io.lumigo java-tracer 1.0.42 jar - Lumigo java tracer The Lumigo java tracer for serverless functions https://lumigo.io/ - https://github.com/lumigo-io/java-tracer scm:git:https://github.com:lumigo-io/java-tracer.git scm:git:https://github.com:lumigo-io/java-tracer.git 1.0.42 - Lumigo Dev Team @@ -28,12 +22,10 @@ https://lumigo.io/ - Lumigo https://lumigo.io/ - Apache License, Version 2.0 @@ -41,8 +33,6 @@ repo - - ossrh @@ -53,8 +43,6 @@ https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - 1.8 1.8 @@ -159,7 +147,7 @@ org.projectlombok lombok - 1.18.22 + 1.18.32 provided @@ -182,7 +170,11 @@ byte-buddy-agent 1.14.14 - + + org.json + json + 20210307 + org.junit.jupiter @@ -233,7 +225,7 @@ **/lumigo-version.txt - + @@ -267,8 +259,7 @@ - - + org.apache.maven.plugins maven-surefire-plugin @@ -297,12 +288,11 @@ 3.0.5 + Enables analysis which takes more memory but finds more bugs. + If you run out of memory, changes the value of the effort element + to 'Low'. + --> Max - Low @@ -312,9 +302,7 @@ findbugs/findbugs-exclude.xml - + analyze-compile compile @@ -327,7 +315,7 @@ org.jacoco jacoco-maven-plugin - 0.8.3 + 0.8.11 @@ -377,4 +365,4 @@ - + \ No newline at end of file diff --git a/scripts/checks.sh b/scripts/checks.sh index bd5e831..01a91bb 100755 --- a/scripts/checks.sh +++ b/scripts/checks.sh @@ -2,5 +2,5 @@ set -eo pipefail java -jar libs/google-java-format-1.7-all-deps.jar --set-exit-if-changed -i -a $(find . -type f -name "*.java" | grep ".*/src/.*java") -mvn -f agent/pom.xml clean package -mvn clean package \ No newline at end of file +mvn -Djava.security.manager=allow -f agent/pom.xml clean package +mvn -Djava.security.manager=allow clean package \ No newline at end of file diff --git a/src/main/java/io/lumigo/core/SpansContainer.java b/src/main/java/io/lumigo/core/SpansContainer.java index acabae0..50a12ef 100644 --- a/src/main/java/io/lumigo/core/SpansContainer.java +++ b/src/main/java/io/lumigo/core/SpansContainer.java @@ -9,9 +9,12 @@ import io.lumigo.core.parsers.v1.AwsSdkV1ParserFactory; import io.lumigo.core.parsers.v2.AwsSdkV2ParserFactory; import io.lumigo.core.utils.AwsUtils; +import io.lumigo.core.utils.EnvUtil; import io.lumigo.core.utils.JsonUtils; +import io.lumigo.core.utils.SecretScrubber; import io.lumigo.core.utils.StringUtils; import io.lumigo.models.HttpSpan; +import io.lumigo.models.Reportable; import io.lumigo.models.Span; import java.io.*; import java.util.*; @@ -38,6 +41,7 @@ public class SpansContainer { private static final String AMZN_TRACE_ID = "_X_AMZN_TRACE_ID"; private static final String FUNCTION_SPAN_TYPE = "function"; private static final String HTTP_SPAN_TYPE = "http"; + private static final SecretScrubber secretScrubber = new SecretScrubber(new EnvUtil().getEnv()); private Span baseSpan; private Span startFunctionSpan; @@ -45,7 +49,6 @@ public class SpansContainer { private Span endFunctionSpan; private Reporter reporter; private List httpSpans = new LinkedList<>(); - private static final SpansContainer ourInstance = new SpansContainer(); public static SpansContainer getInstance() { @@ -68,6 +71,7 @@ private SpansContainer() {} public void init(Map env, Reporter reporter, Context context, Object event) { this.clear(); this.reporter = reporter; + int javaVersion = AwsUtils.parseJavaVersion(System.getProperty("java.version")); if (javaVersion > 11) { awsTracerId = System.getProperty("com.amazonaws.xray.traceHeader"); @@ -214,8 +218,8 @@ public Span getStartFunctionSpan() { return startFunctionSpan; } - public List getAllCollectedSpans() { - List spans = new LinkedList<>(); + public List getAllCollectedSpans() { + List spans = new LinkedList<>(); spans.add(endFunctionSpan); spans.addAll(httpSpans); return spans; @@ -518,63 +522,22 @@ protected static T callIfVerbose(Callable method) { } } - private Object prepareToSend(Object span, boolean hasError) { - return reduceSpanSize(span, hasError); + private Reportable prepareToSend(Reportable span, boolean hasError) { + return reduceSpanSize(span.scrub(secretScrubber), hasError); } - private List prepareToSend(List spans, boolean hasError) { - for (Object span : spans) { - reduceSpanSize(span, hasError); + private List prepareToSend(List spans, boolean hasError) { + for (Reportable span : spans) { + reduceSpanSize(span.scrub(secretScrubber), hasError); } return spans; } - public Object reduceSpanSize(Object span, boolean hasError) { + public Reportable reduceSpanSize(Reportable span, boolean hasError) { int maxFieldSize = hasError ? Configuration.getInstance().maxSpanFieldSizeWhenError() : Configuration.getInstance().maxSpanFieldSize(); - if (span instanceof Span) { - Span functionSpan = (Span) span; - functionSpan.setEnvs( - StringUtils.getMaxSizeString( - functionSpan.getEnvs(), - Configuration.getInstance().maxSpanFieldSize())); - functionSpan.setReturn_value( - StringUtils.getMaxSizeString(functionSpan.getReturn_value(), maxFieldSize)); - functionSpan.setEvent( - StringUtils.getMaxSizeString(functionSpan.getEvent(), maxFieldSize)); - } else if (span instanceof HttpSpan) { - HttpSpan httpSpan = (HttpSpan) span; - httpSpan.getInfo() - .getHttpInfo() - .getRequest() - .setHeaders( - StringUtils.getMaxSizeString( - httpSpan.getInfo().getHttpInfo().getRequest().getHeaders(), - maxFieldSize)); - httpSpan.getInfo() - .getHttpInfo() - .getRequest() - .setBody( - StringUtils.getMaxSizeString( - httpSpan.getInfo().getHttpInfo().getRequest().getBody(), - maxFieldSize)); - httpSpan.getInfo() - .getHttpInfo() - .getResponse() - .setHeaders( - StringUtils.getMaxSizeString( - httpSpan.getInfo().getHttpInfo().getResponse().getHeaders(), - maxFieldSize)); - httpSpan.getInfo() - .getHttpInfo() - .getResponse() - .setBody( - StringUtils.getMaxSizeString( - httpSpan.getInfo().getHttpInfo().getResponse().getBody(), - maxFieldSize)); - } - return span; + return span.reduceSize(maxFieldSize); } } diff --git a/src/main/java/io/lumigo/core/configuration/Configuration.java b/src/main/java/io/lumigo/core/configuration/Configuration.java index 85f8042..55b299f 100644 --- a/src/main/java/io/lumigo/core/configuration/Configuration.java +++ b/src/main/java/io/lumigo/core/configuration/Configuration.java @@ -27,6 +27,7 @@ public class Configuration { public static final String LUMIGO_MAX_RESPONSE_SIZE = "LUMIGO_MAX_RESPONSE_SIZE"; public static final String LUMIGO_MAX_SIZE_FOR_REQUEST = "LUMIGO_MAX_SIZE_FOR_REQUEST"; public static final String LUMIGO_INSTRUMENTATION = "LUMIGO_INSTRUMENTATION"; + public static final String LUMIGO_SECRET_MASKING_REGEX = "LUMIGO_SECRET_MASKING_REGEX"; private static Configuration instance; private LumigoConfiguration inlineConf; diff --git a/src/main/java/io/lumigo/core/network/Reporter.java b/src/main/java/io/lumigo/core/network/Reporter.java index ca9ae86..325efda 100644 --- a/src/main/java/io/lumigo/core/network/Reporter.java +++ b/src/main/java/io/lumigo/core/network/Reporter.java @@ -3,6 +3,7 @@ import io.lumigo.core.configuration.Configuration; import io.lumigo.core.utils.JsonUtils; import io.lumigo.core.utils.StringUtils; +import io.lumigo.models.Reportable; import java.io.IOException; import java.util.Collections; import java.util.LinkedList; @@ -21,11 +22,11 @@ public Reporter() { .build(); } - public long reportSpans(Object span, int maxSize) throws IOException { + public long reportSpans(Reportable span, int maxSize) throws IOException { return reportSpans(Collections.singletonList(span), maxSize); } - public long reportSpans(List spans, int maxSize) throws IOException { + public long reportSpans(List spans, int maxSize) throws IOException { long time = System.currentTimeMillis(); List spansAsStringList = new LinkedList<>(); int sizeCount = 0; diff --git a/src/main/java/io/lumigo/core/utils/SecretScrubber.java b/src/main/java/io/lumigo/core/utils/SecretScrubber.java new file mode 100644 index 0000000..59afa59 --- /dev/null +++ b/src/main/java/io/lumigo/core/utils/SecretScrubber.java @@ -0,0 +1,66 @@ +package io.lumigo.core.utils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import org.json.JSONArray; +import org.json.JSONObject; + +public class SecretScrubber { + private List scrubbingPatterns; + + private static final String SECRET_PLACEHOLDER = "****"; + + public SecretScrubber(Map env) { + this.scrubbingPatterns = new SecretScrubbingPatternProvider(env).getScrubbingPatterns(); + } + + public String scrubStringifiedObject(String stringifiedObject) { + try { + JSONObject jsonObject = new JSONObject(stringifiedObject); + return scrubJsonObject(jsonObject, this.scrubbingPatterns).toString(); + } catch (Exception e) { + return stringifiedObject; + } + } + + private JSONObject scrubJsonObject(JSONObject jsonObject, List patterns) { + for (String key : jsonObject.keySet()) { + Object value = jsonObject.get(key); + + if (value instanceof String && isSecret(key, patterns)) { + jsonObject.put(key, SECRET_PLACEHOLDER); + } else if (value instanceof JSONArray) { + ArrayList scrubbedArray = new ArrayList<>(); + + for (Object item : (JSONArray) value) { + if (item instanceof String && isSecret(key, patterns)) { + scrubbedArray.add(SECRET_PLACEHOLDER); + } else if (item instanceof JSONObject) { + scrubbedArray.add(scrubJsonObject((JSONObject) item, patterns)); + } else { + scrubbedArray.add(item); + } + } + + jsonObject.put(key, scrubbedArray.toArray()); + + } else if (value instanceof JSONObject) { + jsonObject.put(key, scrubJsonObject((JSONObject) value, patterns)); + } + } + + return jsonObject; + } + + private boolean isSecret(String value, List patterns) { + for (Pattern pattern : patterns) { + if (pattern.matcher(value).matches()) { + return true; + } + } + + return false; + } +} diff --git a/src/main/java/io/lumigo/core/utils/SecretScrubbingPatternProvider.java b/src/main/java/io/lumigo/core/utils/SecretScrubbingPatternProvider.java new file mode 100644 index 0000000..564cc11 --- /dev/null +++ b/src/main/java/io/lumigo/core/utils/SecretScrubbingPatternProvider.java @@ -0,0 +1,79 @@ +package io.lumigo.core.utils; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +public class SecretScrubbingPatternProvider { + private static final JsonFactory JSON_FACTORY = new JsonFactory(); + private static final List DEFAULT_PATTERN_STRINGS = + Arrays.asList( + ".*pass.*", + ".*key.*", + ".*secret.*", + ".*credential.*", + ".*passphrase.*", + "SessionToken", + "x-amz-security-token", + "Signature", + "Authorization"); + private static final List DEFAULT_PATTERNS = + stringListToPatterns(DEFAULT_PATTERN_STRINGS); + private final List scrubbingPatterns; + + private static List stringListToPatterns(List patternStrings) { + ArrayList patterns = new ArrayList<>(); + for (String patternString : patternStrings) { + patterns.add(Pattern.compile(patternString, Pattern.CASE_INSENSITIVE)); + } + + return patterns; + } + + private static List jsonListToPatternList(String jsonList) throws IOException { + List patternStrings = new ArrayList(); + + try (JsonParser parser = JSON_FACTORY.createParser(jsonList)) { + if (!JsonToken.START_ARRAY.equals(parser.nextToken())) { + throw new IllegalArgumentException(); + } + while (!JsonToken.END_ARRAY.equals(parser.nextToken())) { + if (parser.currentToken().equals(JsonToken.VALUE_STRING)) { + patternStrings.add(parser.getText()); + } else { + throw new IllegalArgumentException(); + } + } + + return stringListToPatterns(patternStrings); + } + } + + private List buildBodyScrubbingPatterns(Map env) { + String regexStringifiedList = env.get("LUMIGO_SECRET_MASKING_REGEX"); + + if (Strings.isBlank(regexStringifiedList)) { + return DEFAULT_PATTERNS; + } + + try { + return jsonListToPatternList(regexStringifiedList); + } catch (IOException e) { + return DEFAULT_PATTERNS; + } + } + + public List getScrubbingPatterns() { + return this.scrubbingPatterns; + } + + public SecretScrubbingPatternProvider(Map env) { + this.scrubbingPatterns = buildBodyScrubbingPatterns(env); + } +} diff --git a/src/main/java/io/lumigo/core/utils/Strings.java b/src/main/java/io/lumigo/core/utils/Strings.java new file mode 100644 index 0000000..fe505b9 --- /dev/null +++ b/src/main/java/io/lumigo/core/utils/Strings.java @@ -0,0 +1,16 @@ +package io.lumigo.core.utils; + +public class Strings { + public static boolean isBlank(String s) { + int strLen; + if (s == null || (strLen = s.length()) == 0) { + return true; + } + for (int i = 0; i < strLen; i++) { + if (!Character.isWhitespace(s.charAt(i))) { + return false; + } + } + return true; + } +} diff --git a/src/main/java/io/lumigo/models/HttpSpan.java b/src/main/java/io/lumigo/models/HttpSpan.java index f3d4cf6..91989d5 100644 --- a/src/main/java/io/lumigo/models/HttpSpan.java +++ b/src/main/java/io/lumigo/models/HttpSpan.java @@ -1,6 +1,8 @@ package io.lumigo.models; import com.fasterxml.jackson.annotation.JsonProperty; +import io.lumigo.core.utils.SecretScrubber; +import io.lumigo.core.utils.StringUtils; import java.util.Collections; import java.util.List; import lombok.AllArgsConstructor; @@ -10,7 +12,7 @@ @AllArgsConstructor @Builder(toBuilder = true) @Data(staticConstructor = "of") -public class HttpSpan { +public class HttpSpan implements Reportable { private Long started; private Long ended; private String id; @@ -87,4 +89,67 @@ public static class HttpData { private Integer statusCode; private String method; } + + @Override + public Reportable scrub(SecretScrubber scrubber) { + this.getInfo() + .getHttpInfo() + .getRequest() + .setHeaders( + scrubber.scrubStringifiedObject( + this.getInfo().getHttpInfo().getRequest().getHeaders())); + this.getInfo() + .getHttpInfo() + .getRequest() + .setBody( + scrubber.scrubStringifiedObject( + this.getInfo().getHttpInfo().getRequest().getBody())); + this.getInfo() + .getHttpInfo() + .getResponse() + .setHeaders( + scrubber.scrubStringifiedObject( + this.getInfo().getHttpInfo().getResponse().getHeaders())); + this.getInfo() + .getHttpInfo() + .getResponse() + .setBody( + scrubber.scrubStringifiedObject( + this.getInfo().getHttpInfo().getResponse().getBody())); + + return this; + } + + @Override + public Reportable reduceSize(int maxFieldSize) { + this.getInfo() + .getHttpInfo() + .getRequest() + .setHeaders( + StringUtils.getMaxSizeString( + this.getInfo().getHttpInfo().getRequest().getHeaders(), + maxFieldSize)); + this.getInfo() + .getHttpInfo() + .getRequest() + .setBody( + StringUtils.getMaxSizeString( + this.getInfo().getHttpInfo().getRequest().getBody(), maxFieldSize)); + this.getInfo() + .getHttpInfo() + .getResponse() + .setHeaders( + StringUtils.getMaxSizeString( + this.getInfo().getHttpInfo().getResponse().getHeaders(), + maxFieldSize)); + this.getInfo() + .getHttpInfo() + .getResponse() + .setBody( + StringUtils.getMaxSizeString( + this.getInfo().getHttpInfo().getResponse().getBody(), + maxFieldSize)); + + return this; + } } diff --git a/src/main/java/io/lumigo/models/Reportable.java b/src/main/java/io/lumigo/models/Reportable.java new file mode 100644 index 0000000..b73845c --- /dev/null +++ b/src/main/java/io/lumigo/models/Reportable.java @@ -0,0 +1,9 @@ +package io.lumigo.models; + +import io.lumigo.core.utils.SecretScrubber; + +public interface Reportable { + public Reportable scrub(SecretScrubber scrubber); + + public Reportable reduceSize(int maxFieldSize); +} diff --git a/src/main/java/io/lumigo/models/Span.java b/src/main/java/io/lumigo/models/Span.java index a37b84c..cb409da 100644 --- a/src/main/java/io/lumigo/models/Span.java +++ b/src/main/java/io/lumigo/models/Span.java @@ -1,6 +1,10 @@ package io.lumigo.models; import com.fasterxml.jackson.annotation.JsonProperty; +import io.lumigo.core.configuration.Configuration; +import io.lumigo.core.utils.JsonUtils; +import io.lumigo.core.utils.SecretScrubber; +import io.lumigo.core.utils.StringUtils; import java.util.List; import java.util.Locale; import lombok.AllArgsConstructor; @@ -10,7 +14,7 @@ @AllArgsConstructor @Builder(toBuilder = true) @Data(staticConstructor = "of") -public class Span { +public class Span implements Reportable { private String name; private long started; private long ended; @@ -84,4 +88,26 @@ public String toString() { return name().toLowerCase(Locale.ENGLISH); } } + + @Override + public Reportable scrub(SecretScrubber scrubber) { + this.setEnvs( + JsonUtils.getObjectAsJsonString(scrubber.scrubStringifiedObject(this.getEnvs()))); + this.setEvent( + JsonUtils.getObjectAsJsonString(scrubber.scrubStringifiedObject(this.getEvent()))); + this.setReturn_value(scrubber.scrubStringifiedObject(this.getReturn_value())); + + return this; + } + + @Override + public Reportable reduceSize(int maxFieldSize) { + this.setEnvs( + StringUtils.getMaxSizeString( + this.getEnvs(), Configuration.getInstance().maxSpanFieldSize())); + this.setReturn_value(StringUtils.getMaxSizeString(this.getReturn_value(), maxFieldSize)); + this.setEvent(StringUtils.getMaxSizeString(this.getEvent(), maxFieldSize)); + + return this; + } } diff --git a/src/test/java/io/lumigo/core/SpansContainerTest.java b/src/test/java/io/lumigo/core/SpansContainerTest.java index a07fc30..f83c9df 100644 --- a/src/test/java/io/lumigo/core/SpansContainerTest.java +++ b/src/test/java/io/lumigo/core/SpansContainerTest.java @@ -1,6 +1,7 @@ package io.lumigo.core; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Answers.RETURNS_DEEP_STUBS; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @@ -17,6 +18,8 @@ import io.lumigo.handlers.LumigoConfiguration; import io.lumigo.models.HttpSpan; import io.lumigo.models.Span; +import io.lumigo.testUtils.JsonTestUtils; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -29,6 +32,7 @@ import org.apache.http.StatusLine; import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.message.BasicHeader; import org.junit.Assert; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -56,9 +60,15 @@ class SpansContainerTest { @Mock private EnvUtil envUtil; @Mock private Context context; @Mock Reporter reporter; - @Mock HttpResponse httpResponse; + + @Mock(answer = RETURNS_DEEP_STUBS) + HttpResponse httpResponse; + @Mock StatusLine statusLine; - @Mock HttpUriRequest httpRequest; + + @Mock(answer = RETURNS_DEEP_STUBS) + HttpEntityEnclosingRequestBase httpRequest; + @Mock Request awsRequest; @Mock com.amazonaws.http.HttpResponse awsHttpResponse; @@ -83,7 +93,10 @@ void clear() throws JsonProcessingException { @DisplayName("Check that start span include all relevant data") @Test void createStartSpan() throws Exception { - spansContainer.init(createMockedEnv(), reporter, context, null); + Map env = createMockedEnv(); + env.put("SOME_KEY", "s0m3t0k3y"); + + spansContainer.init(env, reporter, context, "{\"secret\":\"stuff\"}"); spansContainer.start(); Span actualSpan = spansContainer.getStartFunctionSpan(); @@ -94,14 +107,14 @@ void createStartSpan() throws Exception { + " \"ended\": 1557823871416,\n" + " \"runtime\": \"JAVA8\",\n" + " \"id\": \"3n2783hf7823hdui32_started\",\n" - + " \"type\": function,\n" + + " \"type\": \"function\",\n" + " \"memoryAllocated\": \"100\",\n" + " \"transactionId\": \"3\",\n" + " \"requestId\": \"3n2783hf7823hdui32\",\n" + " \"account\": \"1111\",\n" + " \"maxFinishTime\": 100,\n" - + " \"event\": null,\n" - + " \"envs\": \"{\\\"AWS_REGION\\\":\\\"us-west-2\\\",\\\"_X_AMZN_TRACE_ID\\\":\\\"Root=1-2-3;Another=456;Bla=789\\\",\\\"AWS_EXECUTION_ENV\\\":\\\"JAVA8\\\"}\",\n" + + " \"event\": \"{\\\"secret\\\":\\\"****\\\"}\",\n" + + " \"envs\": \"{\\\"AWS_REGION\\\":\\\"us-west-2\\\",\\\"_X_AMZN_TRACE_ID\\\":\\\"Root=1-2-3;Another=456;Bla=789\\\",\\\"AWS_EXECUTION_ENV\\\":\\\"JAVA8\\\",\\\"SOME_KEY\\\":\\\"****\\\"}\",\n" + " \"region\": \"us-west-2\",\n" + " \"reporter_rtt\": null,\n" + " \"error\": null,\n" @@ -116,7 +129,7 @@ void createStartSpan() throws Exception { + " },\n" + " \"logStreamName\": \"2019/05/12/[$LATEST]7f67fc1238a941749d8126be19f0cdc6\",\n" + " \"logGroupName\": \"/aws/lambda/mocked_function_name\",\n" - + " \"triggeredBy\": null\n" + + " \"triggeredBy\": \"No recognized trigger\"\n" + " }\n" + "}"; long started = actualSpan.getStarted(); @@ -131,7 +144,8 @@ void createStartSpan() throws Exception { new Customization( "maxFinishTime", (o1, o2) -> started + 100 == Long.valueOf(o1.toString())), - new Customization("ended", (o1, o2) -> o2 != null))); + new Customization("ended", (o1, o2) -> o2 != null), + new Customization("envs", JsonTestUtils::compareJsonStrings))); } @DisplayName("End span which contains error") @@ -190,6 +204,7 @@ void endWithException() throws Exception { new Customization( "maxFinishTime", (o1, o2) -> started + 100 == Long.valueOf(o1.toString())), + new Customization("envs", JsonTestUtils::compareJsonStrings), new Customization("error.stacktrace", (o1, o2) -> o1 != null))); } @@ -245,14 +260,18 @@ void end() throws Exception { "maxFinishTime", (o1, o2) -> started + 100 == Long.valueOf(o1.toString())), new Customization("token", (o1, o2) -> o1 != null), + new Customization("envs", JsonTestUtils::compareJsonStrings), new Customization("error.stacktrace", (o1, o2) -> o1 != null))); } @DisplayName("End span creation with return value") @Test void end_with_return_value() throws Exception { - spansContainer.init(createMockedEnv(), reporter, context, null); - spansContainer.end("RESULT"); + Map env = createMockedEnv(); + env.put("Authorization", "secret$token"); + + spansContainer.init(env, reporter, context, null); + spansContainer.end("{\"credentials\":\"user:password\"}"); Span actualSpan = spansContainer.getEndSpan(); String expectedSpan = @@ -269,12 +288,12 @@ void end_with_return_value() throws Exception { + " \"account\": \"1111\",\n" + " \"maxFinishTime\": 100,\n" + " \"event\": null,\n" - + " \"envs\": \"{\\\"AWS_REGION\\\":\\\"us-west-2\\\",\\\"_X_AMZN_TRACE_ID\\\":\\\"Root=1-2-3;Another=456;Bla=789\\\",\\\"AWS_EXECUTION_ENV\\\":\\\"JAVA8\\\"}\",\n" + + " \"envs\": \"{\\\"AWS_EXECUTION_ENV\\\":\\\"JAVA8\\\",\\\"AWS_REGION\\\":\\\"us-west-2\\\",\\\"_X_AMZN_TRACE_ID\\\":\\\"Root=1-2-3;Another=456;Bla=789\\\",\\\"Authorization\\\":\\\"****\\\"}\",\n" + " \"region\": \"us-west-2\",\n" + " \"reporter_rtt\": null,\n" + " \"error\": null,\n" + " \"token\": null,\n" - + " \"return_value\": \"RESULT\",\n" + + " \"return_value\": \"{\\\"credentials\\\":\\\"****\\\"}\",\n" + " \"info\": {\n" + " \"tracer\": {\n" + " \"version\": \"1.0\"\n" @@ -297,6 +316,7 @@ void end_with_return_value() throws Exception { new Customization("started", (o1, o2) -> o1 != null), new Customization("ended", (o1, o2) -> o1 != null), new Customization("token", (o1, o2) -> o1 != null), + new Customization("envs", JsonTestUtils::compareJsonStrings), new Customization( "maxFinishTime", (o1, o2) -> started + 100 == Long.valueOf(o1.toString())), @@ -308,13 +328,31 @@ void end_with_return_value() throws Exception { void add_http_span() throws Exception { spansContainer.init(createMockedEnv(), reporter, context, null); when(httpRequest.getURI()).thenReturn(URI.create("https://google.com")); + when(httpRequest.getEntity().getContent()) + .thenReturn(new ByteArrayInputStream("{\"passphrase\":\"value\"}".getBytes())); + when(httpRequest.getAllHeaders()) + .thenReturn( + new Header[] { + new BasicHeader("authorization", "token"), + new BasicHeader("x-some-thing", "sent") + }); when(httpResponse.getStatusLine()).thenReturn(statusLine); - when(httpResponse.getAllHeaders()).thenReturn(new Header[0]); + when(httpResponse.getAllHeaders()) + .thenReturn( + new Header[] { + new BasicHeader("credentials", "user:pazzword"), + new BasicHeader("x-some-stuff", "returned") + }); + when(httpResponse.getEntity().getContent()) + .thenReturn(new ByteArrayInputStream("{\"password\":\"value\"}".getBytes())); when(statusLine.getStatusCode()).thenReturn(200); long startTime = System.currentTimeMillis(); spansContainer.addHttpSpan(startTime, httpRequest, httpResponse); + // Trigger scrubbing + spansContainer.end(); + HttpSpan actualSpan = spansContainer.getHttpSpans().get(0); String expectedSpan = "{\n" @@ -336,15 +374,15 @@ void add_http_span() throws Exception { + " \"httpInfo\":{\n" + " \"host\":\"google.com\",\n" + " \"request\":{\n" - + " \"headers\":\"{}\",\n" - + " \"body\":null,\n" + + " \"headers\":\"{\\\"authorization\\\":\\\"****\\\",\\\"x-some-thing\\\":\\\"sent\\\"}\",\n" + + " \"body\":\"{\\\"passphrase\\\":\\\"****\\\"}\",\n" + " \"uri\":\"https://google.com\",\n" + " \"statusCode\":null,\n" + " \"method\":null\n" + " },\n" + " \"response\":{\n" - + " \"headers\":\"{}\",\n" - + " \"body\":null,\n" + + " \"headers\":\"{\\\"credentials\\\":\\\"****\\\",\\\"x-some-stuff\\\":\\\"returned\\\"}\",\n" + + " \"body\":\"{\\\"password\\\":\\\"****\\\"}\",\n" + " \"uri\":null,\n" + " \"statusCode\":200,\n" + " \"method\":null\n" @@ -375,10 +413,14 @@ void add_aws_http_span_with_spnid_from_header_amzn() throws Exception { when(awsRequest.getHttpMethod()).thenReturn(HttpMethodName.GET); when(awsHttpResponse.getStatusCode()).thenReturn(200); when(awsHttpResponse.getHeaders()).thenReturn(headers); - Response awsResponse = new Response<>("awsResponse", awsHttpResponse); + Response awsResponse = + new Response<>("{\"passphrase\":\"some-token\"}", awsHttpResponse); long startTime = System.currentTimeMillis(); spansContainer.addHttpSpan(startTime, awsRequest, awsResponse); + // Applies scrubbing + spansContainer.end(); + HttpSpan actualSpan = spansContainer.getHttpSpans().get(0); String expectedSpan = "{\n" @@ -410,7 +452,7 @@ void add_aws_http_span_with_spnid_from_header_amzn() throws Exception { + " },\n" + " \"response\":{\n" + " \"headers\":\"{\\\"x-amzn-requestid\\\":\\\"id123\\\"}\",\n" - + " \"body\":\"awsResponse\",\n" + + " \"body\":\"{\\\"passphrase\\\":\\\"****\\\"}\",\n" + " \"uri\":null,\n" + " \"statusCode\":200,\n" + " \"method\":null\n" @@ -499,6 +541,7 @@ void add_aws_http_span_with_spnid_from_header_amz() throws Exception { void add_aws_sdk_v2_http_span() throws Exception { Map> headers = new HashMap<>(); headers.put("x-amz-requestid", Collections.singletonList("id123")); + headers.put("credentials", Collections.singletonList("user:pass")); PublishRequest request = PublishRequest.builder().topicArn("topic").build(); PublishResponse response = @@ -531,6 +574,9 @@ void add_aws_sdk_v2_http_span() throws Exception { spansContainer.addHttpSpan(startTime, requestContext, executionAttributes); + // Triggers scrubbing + spansContainer.end(); + HttpSpan actualSpan = spansContainer.getHttpSpans().get(0); String expectedSpan = "{\n" @@ -552,14 +598,14 @@ void add_aws_sdk_v2_http_span() throws Exception { + " \"httpInfo\":{\n" + " \"host\":\"sns.amazonaws.com\",\n" + " \"request\":{\n" - + " \"headers\":\"{\\\"x-amz-requestid\\\":[\\\"id123\\\"]}\",\n" + + " \"headers\":\"{\\\"credentials\\\":[\\\"****\\\"],\\\"x-amz-requestid\\\":[\\\"id123\\\"]}\",\n" + " \"body\":null,\n" + " \"uri\":\"https://sns.amazonaws.com\",\n" + " \"statusCode\":null,\n" + " \"method\":GET\n" + " },\n" + " \"response\":{\n" - + " \"headers\":\"{\\\"x-amz-requestid\\\":[\\\"id123\\\"]}\",\n" + + " \"headers\":\"{\\\"credentials\\\":[\\\"****\\\"],\\\"x-amz-requestid\\\":[\\\"id123\\\"]}\",\n" + " \"body\":\"{\\\"messageId\\\":\\\"fee47356-6f6a-58c8-96dc-26d8aaa4631a\\\",\\\"sequenceNumber\\\":null}\", \n" + " \"uri\":null,\n" + " \"statusCode\":200,\n" @@ -578,6 +624,8 @@ void add_aws_sdk_v2_http_span() throws Exception { new Customization("info.tracer.version", (o1, o2) -> o1 != null), new Customization("id", (o1, o2) -> o1.equals("id123")), new Customization("started", (o1, o2) -> o1 != null), + new Customization( + "info.httpInfo.response.body", JsonTestUtils::compareJsonStrings), new Customization("ended", (o1, o2) -> o1 != null))); } diff --git a/src/test/java/io/lumigo/core/utils/SecretScrubberTest.java b/src/test/java/io/lumigo/core/utils/SecretScrubberTest.java new file mode 100644 index 0000000..c435138 --- /dev/null +++ b/src/test/java/io/lumigo/core/utils/SecretScrubberTest.java @@ -0,0 +1,105 @@ +package io.lumigo.core.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.lumigo.core.configuration.Configuration; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class SecretScrubberTest { + static final Map noScrubbingOverridesEnv = new HashMap(); + + @Test + @DisplayName("does not modify non-json payloads") + void testSecretScrubbingUtils_does_not_scrub_non_json_payloads() { + assertEquals( + "123", new SecretScrubber(noScrubbingOverridesEnv).scrubStringifiedObject("123")); + } + + @Test + @DisplayName("scrubs a nested body value with default expressions") + void testSecretScrubbingUtils_scrubs_json_payload_default() { + String actual = + new SecretScrubber(noScrubbingOverridesEnv) + .scrubStringifiedObject( + "{\"pass\":\"word\",\"key\":\"value\",\"secret\":\"stuff\",\"credential\":\"admin:admin\",\"passphrase\":\"SesameOpen\",\"SessionToken\":\"XyZ012x=\",\"x-amz-security-token\":\"amzToken123\",\"Signature\":\"yours truly\",\"authorization\":\"Bearer 123\"}"); + String expected = + "{\"authorization\":\"****\",\"credential\":\"****\",\"pass\":\"****\",\"SessionToken\":\"****\",\"Signature\":\"****\",\"passphrase\":\"****\",\"secret\":\"****\",\"x-amz-security-token\":\"****\",\"key\":\"****\"}"; + + assertEquals(expected, actual); + } + + @Test + @DisplayName( + "scrubs body using the default patterns when LUMIGO_SECRET_MASKING_REGEX is invalid") + void + testSecretScrubbingUtils_scrubs_json_payload_default_used_when_invalid_pattern_list_provided() { + String actual = + new SecretScrubber(envWithMaskingRegex("[THIS IS NOT A VALID JSON ARRAY]")) + .scrubStringifiedObject( + "{\"pass\":\"word\",\"key\":\"value\",\"secret\":\"stuff\",\"credential\":\"admin:admin\",\"passphrase\":\"SesameOpen\",\"SessionToken\":\"XyZ012x=\",\"x-amz-security-token\":\"amzToken123\",\"Signature\":\"yours truly\",\"authorization\":\"Bearer 123\"}"); + String expected = + "{\"authorization\":\"****\",\"credential\":\"****\",\"pass\":\"****\",\"SessionToken\":\"****\",\"Signature\":\"****\",\"passphrase\":\"****\",\"secret\":\"****\",\"x-amz-security-token\":\"****\",\"key\":\"****\"}"; + + assertEquals(expected, actual); + } + + @Test + @DisplayName("scrubs body using default expressions - case insensitive") + void testSecretScrubbingUtils_scrubs_json_payload_default_case_insensitive() { + String actual = + new SecretScrubber(noScrubbingOverridesEnv) + .scrubStringifiedObject( + "{\"pAss\":\"word\",\"KeY\":\"value\",\"seCRet\":\"stuff\",\"CrEdEntial\":\"admin:admin\",\"paSSPhrase\":\"SesameOpen\",\"seSSIOntOkEn\":\"XyZ012x=\",\"X-AMZ-security-token\":\"amzToken123\",\"SIGnatUre\":\"yours truly\",\"AuTHOrization\":\"Bearer 123\"}"); + String expected = + "{\"pAss\":\"****\",\"CrEdEntial\":\"****\",\"paSSPhrase\":\"****\",\"seSSIOntOkEn\":\"****\",\"SIGnatUre\":\"****\",\"seCRet\":\"****\",\"X-AMZ-security-token\":\"****\",\"KeY\":\"****\",\"AuTHOrization\":\"****\"}"; + + assertEquals(expected, actual); + } + + @Test + @DisplayName("scrubs body using LUMIGO_SECRET_MASKING_REGEX - top-level key") + void testSecretScrubbingUtils_scrubs_json_payload_top_level_key() { + String actual = + new SecretScrubber(envWithMaskingRegex("[\".*topsecret.*\"]")) + .scrubStringifiedObject("{\"topsecret\":\"stuff\"}"); + String expected = "{\"topsecret\":\"****\"}"; + + assertEquals(expected, actual); + } + + @Test + @DisplayName("scrubs body using LUMIGO_SECRET_MASKING_REGEX - nested keys") + void testSecretScrubbingUtils_scrubs_json_payload_nested_key() { + String actual = + new SecretScrubber(envWithMaskingRegex("[\".*topsecret.*\"]")) + .scrubStringifiedObject("{\"some\": {\"topsecret\":\"stuff\"}, \"a\": 1}"); + + String expected = "{\"some\":{\"topsecret\":\"****\"},\"a\":1}"; + + assertEquals(expected, actual); + } + + @Test + @DisplayName("scrubs arrays using default expressions") + void testSecretScrubbingUtils_scrubs_arrays() { + String actual = + new SecretScrubber(envWithMaskingRegex("[\".*topsecret.*\"]")) + .scrubStringifiedObject( + "{\"some\": [{\"topsecret\":\"stuff\"}, {\"a\":1}]}"); + + String expected = "{\"some\":[{\"topsecret\":\"****\"},{\"a\":1}]}"; + + assertEquals(expected, actual); + } + + private Map envWithMaskingRegex(String maskingRegex) { + return new HashMap() { + { + put(Configuration.LUMIGO_SECRET_MASKING_REGEX, maskingRegex); + } + }; + } +} diff --git a/src/test/java/io/lumigo/handlers/LumigoRequestHandlerTest.java b/src/test/java/io/lumigo/handlers/LumigoRequestHandlerTest.java index 9d9c42f..58910cb 100644 --- a/src/test/java/io/lumigo/handlers/LumigoRequestHandlerTest.java +++ b/src/test/java/io/lumigo/handlers/LumigoRequestHandlerTest.java @@ -2,6 +2,8 @@ import static io.lumigo.core.utils.AwsUtils.COLD_START_KEY; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.*; import com.amazonaws.services.lambda.runtime.Context; @@ -13,7 +15,9 @@ import io.lumigo.core.network.Reporter; import io.lumigo.core.utils.EnvUtil; import io.lumigo.core.utils.JsonUtils; +import io.lumigo.models.Reportable; import io.lumigo.models.Span; +import io.lumigo.testUtils.JsonTestUtils; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -147,6 +151,7 @@ public Void handleRequest(KinesisEvent kinesisEvent, Context context) { return LumigoRequestExecutor.execute(kinesisEvent, context, supplier); } } + /** * ************************************* * @@ -243,7 +248,9 @@ public void LumigoRequestHandler_with_response_happy_flow() throws Exception { new Customization("info.messageIds", (o1, o2) -> o2 != null), new Customization("started", (o1, o2) -> o2 != null), new Customization("maxFinishTime", (o1, o2) -> o2 != null), - new Customization("ended", (o1, o2) -> o2 != null))); + new Customization("ended", (o1, o2) -> o2 != null), + new Customization("event", JsonTestUtils::compareJsonStrings), + new Customization("envs", JsonTestUtils::compareJsonStrings))); Span endSpan = getEndSpan("Response", null); endSpan.setReporter_rtt(999L); JSONAssert.assertEquals( @@ -257,7 +264,9 @@ public void LumigoRequestHandler_with_response_happy_flow() throws Exception { new Customization("info.messageIds", (o1, o2) -> o2 != null), new Customization("started", (o1, o2) -> o2 != null), new Customization("maxFinishTime", (o1, o2) -> o2 != null), - new Customization("ended", (o1, o2) -> o2 != null))); + new Customization("ended", (o1, o2) -> o2 != null), + new Customization("event", JsonTestUtils::compareJsonStrings), + new Customization("envs", JsonTestUtils::compareJsonStrings))); } @DisplayName( @@ -288,7 +297,9 @@ public void LumigoRequestHandler_with_exception_happy_flow() throws Exception { new Customization("info.messageIds", (o1, o2) -> o2 != null), new Customization("started", (o1, o2) -> o2 != null), new Customization("maxFinishTime", (o1, o2) -> o2 != null), - new Customization("ended", (o1, o2) -> o2 != null))); + new Customization("ended", (o1, o2) -> o2 != null), + new Customization("event", JsonTestUtils::compareJsonStrings), + new Customization("envs", JsonTestUtils::compareJsonStrings))); JSONAssert.assertEquals( JsonUtils.getObjectAsJsonString( getEndSpan(null, new UnsupportedOperationException())), @@ -302,13 +313,15 @@ public void LumigoRequestHandler_with_exception_happy_flow() throws Exception { new Customization("started", (o1, o2) -> o2 != null), new Customization("error.stacktrace", (o1, o2) -> o2 != null), new Customization("maxFinishTime", (o1, o2) -> o2 != null), - new Customization("ended", (o1, o2) -> o2 != null))); + new Customization("ended", (o1, o2) -> o2 != null), + new Customization("event", JsonTestUtils::compareJsonStrings), + new Customization("envs", JsonTestUtils::compareJsonStrings))); } @DisplayName( "Create a handler that return a response, Lumigo tracer is configuration is inline and tracer send relevant spans") @Test - public void LumigoRequestHandler_with_inline_configuration_return_reponse_happy_flow() + public void LumigoRequestHandler_with_inline_configuration_return_response_happy_flow() throws Exception { HandlerStaticInit handler = new HandlerStaticInit(); handler.setEnvUtil(envUtil); @@ -440,10 +453,12 @@ public void LumigoRequestStreamHandler_happy_flow_response() throws Exception { handler.handleRequest(null, null, context); - ArgumentCaptor argumentCaptorAllSpans = ArgumentCaptor.forClass(List.class); - ArgumentCaptor argumentCaptorStartSpan = ArgumentCaptor.forClass(Span.class); - verify(reporter, Mockito.times(1)).reportSpans(argumentCaptorAllSpans.capture(), anyInt()); + ArgumentCaptor> argumentCaptorAllSpans = + ArgumentCaptor.forClass(List.class); + ArgumentCaptor argumentCaptorStartSpan = + ArgumentCaptor.forClass(Reportable.class); verify(reporter, Mockito.times(1)).reportSpans(argumentCaptorStartSpan.capture(), anyInt()); + verify(reporter, Mockito.times(1)).reportSpans(argumentCaptorAllSpans.capture(), anyInt()); JSONAssert.assertEquals( JsonUtils.getObjectAsJsonString( @@ -454,7 +469,9 @@ public void LumigoRequestStreamHandler_happy_flow_response() throws Exception { new Customization("info.tracer.version", (o1, o2) -> o2 != null), new Customization("started", (o1, o2) -> o2 != null), new Customization("maxFinishTime", (o1, o2) -> o2 != null), - new Customization("ended", (o1, o2) -> o2 != null))); + new Customization("ended", (o1, o2) -> o2 != null), + new Customization("envs", JsonTestUtils::compareJsonStrings))); + JSONAssert.assertEquals( JsonUtils.getObjectAsJsonString( getEndSpan(null, null, false) @@ -469,7 +486,8 @@ public void LumigoRequestStreamHandler_happy_flow_response() throws Exception { new Customization("info.tracer.version", (o1, o2) -> o2 != null), new Customization("started", (o1, o2) -> o2 != null), new Customization("maxFinishTime", (o1, o2) -> o2 != null), - new Customization("ended", (o1, o2) -> o2 != null))); + new Customization("ended", (o1, o2) -> o2 != null), + new Customization("envs", JsonTestUtils::compareJsonStrings))); } @DisplayName("Create a handler that throw exception, Lumigo tracer send spans successfully") @@ -498,7 +516,8 @@ public void LumigoRequestStreamHandler_happy_flow_error() throws Exception { new Customization("info.tracer.version", (o1, o2) -> o2 != null), new Customization("started", (o1, o2) -> o2 != null), new Customization("maxFinishTime", (o1, o2) -> o2 != null), - new Customization("ended", (o1, o2) -> o2 != null))); + new Customization("ended", (o1, o2) -> o2 != null), + new Customization("envs", JsonTestUtils::compareJsonStrings))); JSONAssert.assertEquals( JsonUtils.getObjectAsJsonString( getEndSpan(null, new UnsupportedOperationException(), false) @@ -514,7 +533,8 @@ public void LumigoRequestStreamHandler_happy_flow_error() throws Exception { new Customization("started", (o1, o2) -> o2 != null), new Customization("error.stacktrace", (o1, o2) -> o2 != null), new Customization("maxFinishTime", (o1, o2) -> o2 != null), - new Customization("ended", (o1, o2) -> o2 != null))); + new Customization("ended", (o1, o2) -> o2 != null), + new Customization("envs", JsonTestUtils::compareJsonStrings))); } @DisplayName( @@ -744,7 +764,10 @@ public void LumigoRequestExecutor_with_response_happy_flow() throws Exception { new Customization("info.messageIds", (o1, o2) -> o2 != null), new Customization("started", (o1, o2) -> o2 != null), new Customization("maxFinishTime", (o1, o2) -> o2 != null), - new Customization("ended", (o1, o2) -> o2 != null))); + new Customization("ended", (o1, o2) -> o2 != null), + new Customization("event", JsonTestUtils::compareJsonStrings), + new Customization("envs", JsonTestUtils::compareJsonStrings))); + Span endSpan = getEndSpan("Response", null); endSpan.setReporter_rtt(999L); JSONAssert.assertEquals( @@ -758,7 +781,9 @@ public void LumigoRequestExecutor_with_response_happy_flow() throws Exception { new Customization("info.messageIds", (o1, o2) -> o2 != null), new Customization("started", (o1, o2) -> o2 != null), new Customization("maxFinishTime", (o1, o2) -> o2 != null), - new Customization("ended", (o1, o2) -> o2 != null))); + new Customization("ended", (o1, o2) -> o2 != null), + new Customization("event", JsonTestUtils::compareJsonStrings), + new Customization("envs", JsonTestUtils::compareJsonStrings))); } @DisplayName( @@ -789,7 +814,9 @@ public void LumigoRequestExecutor_with_exception_happy_flow() throws Exception { new Customization("info.messageIds", (o1, o2) -> o2 != null), new Customization("maxFinishTime", (o1, o2) -> o2 != null), new Customization("started", (o1, o2) -> o2 != null), - new Customization("ended", (o1, o2) -> o2 != null))); + new Customization("ended", (o1, o2) -> o2 != null), + new Customization("event", JsonTestUtils::compareJsonStrings), + new Customization("envs", JsonTestUtils::compareJsonStrings))); JSONAssert.assertEquals( JsonUtils.getObjectAsJsonString( getEndSpan(null, new UnsupportedOperationException())), @@ -803,7 +830,9 @@ public void LumigoRequestExecutor_with_exception_happy_flow() throws Exception { new Customization("maxFinishTime", (o1, o2) -> o2 != null), new Customization("started", (o1, o2) -> o2 != null), new Customization("error.stacktrace", (o1, o2) -> o2 != null), - new Customization("ended", (o1, o2) -> o2 != null))); + new Customization("ended", (o1, o2) -> o2 != null), + new Customization("event", JsonTestUtils::compareJsonStrings), + new Customization("envs", JsonTestUtils::compareJsonStrings))); } @DisplayName( diff --git a/src/test/java/io/lumigo/testUtils/JsonTestUtils.java b/src/test/java/io/lumigo/testUtils/JsonTestUtils.java new file mode 100644 index 0000000..2bf6982 --- /dev/null +++ b/src/test/java/io/lumigo/testUtils/JsonTestUtils.java @@ -0,0 +1,27 @@ +package io.lumigo.testUtils; + +import org.json.JSONArray; +import org.json.JSONObject; + +public class JsonTestUtils { + + public static boolean compareJsonStrings(Object o1, Object o2) { + if (o1 == null || o2 == null) { + return o1 == o2; + } + + try { + JSONObject json1 = new JSONObject((String) o1); + JSONObject json2 = new JSONObject((String) o2); + return json1.similar(json2); + } catch (Exception e1) { + try { + JSONArray json1 = new JSONArray((String) o1); + JSONArray json2 = new JSONArray((String) o2); + return json1.similar(json2); + } catch (Exception e2) { + return false; + } + } + } +}