diff --git a/Changelog.md b/Changelog.md index 7a8c5562d..d2d1a2847 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,8 +4,52 @@ Noteworthy changes to the agent are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.0.6-public-preview] - TO BE DISCLOSED + +## [1.0.8-public-preview] TO BE DECIDED +### Changes +- Support for stored procedure call detection in SQL events +- Support for extracting environment variables in case of Remote Code Execution events +- Support for executing script file analysis in case of Remote Code Execution events +- Enabled the transformation of the low-priority instrumentation module by default in case of IAST +- SecureCookie schema check has been removed +### Fixes +- Incorrect user file details in the vulnerability details +- Low severity hook event was not generated when the same url can process multiple request methods +- Detection of server app directory to mitigate false positives for File Access vulnerability + +## [1.0.7-public-preview] - 2023-12-6 ### Changes +- Async HttpClient v2+ Support: The security agent now also supports Async HTTP client version 2 and above +- Sun Net HTTP Server support: The security agent now supports Sun Net HTTP Server +- Add APM trace information population in the event +- WS headers added : NR-CSEC-ENTITY-GUID & NR-CSEC-ENTITY-NAME +- JSON version bump to 1.1.1 +- Add critical error logging via LogMessage event +### Fixes +- Insecure cookie attack vulnerability was flagged in secure communication, accounting communication type to mitigate the issue +- DynamoDB v2 issue: missing attribute values for conditionCheck method in case of transactWriteItems operation on DynamoDB +- Never print LicenseKey +### Misc +- Updated unit test cases for all the outbound request instrumentation modules to include test cases for csec parent id header +- Unit test cases for Async HttpClient v2+ +- Unit test cases for Jetty v12+ +- Unit test cases for Sun Net HTTP Server +- Unit test cases for Netty Server + +## [1.0.6-public-preview] - 2023-10-17 +### Changes +- Cassandra DB v3.0+ Support: The Security agent now supports Cassandra DB version 3.0 and above +- HttpClient v5.0+ Support: The Security agent now also supports HttpClient version 5.0 and above +- Support for std-out logging +- Added feature for Daily log rollover +- Support for logger config: log_file_count and log_limit_in_kbytes +- Relocating all our instrumentation packages under the package com.newrelic.agent.security.instrumentation.* +- Package Refactoring for Unit Tests: Move packaging for all UTs to com.nr.agent.security.instrumentation.* +- Set default value for low severity instrumentation to false + +### Fixes +- Fixed ClassNotFoundException for IOStreamHelper class with Glassfish +- Updated PostgreSQL UTs with Embedded Server instead of test container ## [1.0.5-public-preview] - 2023-08-29 ### Changes diff --git a/README.md b/README.md index 3307c5396..c59cd5c78 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ The agent automatically instruments the following frameworks. - Log4j from 2.0 to 2.20.0 - Servlet from 2.4 to latest - Spring from 0 to latest +- Sun Net HTTP Server ### Java Native Operations @@ -55,6 +56,7 @@ The agent automatically instruments the following HTTP clients and messaging ser - Jaxen XPATH from 1.1 to latest - Saxpath 1.0 - Xalan XPATH 2.1.0 to latest +- Async Http Client from 2.0 to latest ### Datastores diff --git a/gradle.properties b/gradle.properties index 08b6aaa21..8aaddbf59 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,13 +1,13 @@ # The agent version. -agentVersion=1.0.6 -jsonVersion=1.1.0 +agentVersion=1.0.8 +jsonVersion=1.1.1 # Updated exposed NR APM API version. -nrAPIVersion=8.3.0 +nrAPIVersion=8.4.0 # Actual NR APM Agent version # This is intentionally kept to an older NR agent version since it is only used as dependency for unit test framework & # verify instrumentation plugin. This will only be updated when either of these functions require the update. -nrAgentVersion=8.3.0 +nrAgentVersion=8.4.0 #org.gradle.jvmargs=-Xmx2048m org.gradle.jvmargs=-Xmx4g org.gradle.caching=true diff --git a/instrumentation-security-test/src/main/java/com/newrelic/agent/security/introspec/internal/HttpServerRule.java b/instrumentation-security-test/src/main/java/com/newrelic/agent/security/introspec/internal/HttpServerRule.java index 3c5da1950..580153713 100644 --- a/instrumentation-security-test/src/main/java/com/newrelic/agent/security/introspec/internal/HttpServerRule.java +++ b/instrumentation-security-test/src/main/java/com/newrelic/agent/security/introspec/internal/HttpServerRule.java @@ -30,6 +30,12 @@ protected void after() { @Override public void shutdown() { + try { + // to prevent socket.io: broken pipe error for async calls + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } server.shutdown(); } diff --git a/instrumentation-security-test/src/main/java/com/newrelic/agent/security/introspec/internal/HttpTestServerImpl.java b/instrumentation-security-test/src/main/java/com/newrelic/agent/security/introspec/internal/HttpTestServerImpl.java index 12bd4d2c8..18deb125d 100644 --- a/instrumentation-security-test/src/main/java/com/newrelic/agent/security/introspec/internal/HttpTestServerImpl.java +++ b/instrumentation-security-test/src/main/java/com/newrelic/agent/security/introspec/internal/HttpTestServerImpl.java @@ -32,7 +32,7 @@ class HttpTestServerImpl extends NanoHTTPD implements HttpTestServer { private final int port; - private Map headers = new HashMap<>(); + private static Map headers = new HashMap<>(); public HttpTestServerImpl() throws IOException { this(getRandomPort()); @@ -83,7 +83,7 @@ private Response serveNonDispatcher(IHTTPSession session) { private Response serveInternal(IHTTPSession session) { NewRelic.addCustomParameter("server.port", this.port); final Map incomingHeaders = session.getHeaders(); - headers = incomingHeaders; + headers.putAll(incomingHeaders); if (incomingHeaders.containsKey(SLEEP_MS_HEADER_KEY)) { try { diff --git a/instrumentation-security/async-http-client-2.0.0/build.gradle b/instrumentation-security/async-http-client-2.0.0/build.gradle new file mode 100644 index 000000000..1de825698 --- /dev/null +++ b/instrumentation-security/async-http-client-2.0.0/build.gradle @@ -0,0 +1,27 @@ +dependencies { + implementation(project(":newrelic-security-api")) + implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}") + implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}") + implementation("org.asynchttpclient:async-http-client:2.0.0") +} + +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.async-http-client-2.0.0' } +} + +verifyInstrumentation { + passesOnly 'org.asynchttpclient:async-http-client:[2.0.0-RC1,)' + excludeRegex ".*(alpha|RC).*" +} + +test { + // These instrumentation tests only run on Java 8 regardless of the -PtestN gradle property that is set. + onlyIf { + project.hasProperty('test8') + } +} + +site { + title 'Async Http Client' + type 'Messaging' +} diff --git a/instrumentation-security/async-http-client-2.0.0/src/main/java/com/newrelic/agent/security/instrumentation/org/asynchttpclient/AsynchttpHelper.java b/instrumentation-security/async-http-client-2.0.0/src/main/java/com/newrelic/agent/security/instrumentation/org/asynchttpclient/AsynchttpHelper.java new file mode 100644 index 000000000..743816e50 --- /dev/null +++ b/instrumentation-security/async-http-client-2.0.0/src/main/java/com/newrelic/agent/security/instrumentation/org/asynchttpclient/AsynchttpHelper.java @@ -0,0 +1,122 @@ +package com.newrelic.agent.security.instrumentation.org.asynchttpclient; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.StringUtils; +import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityException; +import com.newrelic.api.agent.security.schema.operation.SSRFOperation; +import com.newrelic.api.agent.security.utils.SSRFUtils; +import org.asynchttpclient.Request; + +public class AsynchttpHelper { + + + public static final String NR_SEC_CUSTOM_ATTRIB_NAME = "ASYNCHTTP_OPERATION_LOCK-"; + + public static final String METHOD_EXECUTE = "executeRequest"; + + public static boolean skipExistsEvent() { + if (!(NewRelicSecurity.getAgent().getCurrentPolicy().getVulnerabilityScan().getEnabled() && + NewRelicSecurity.getAgent().getCurrentPolicy().getVulnerabilityScan().getIastScan().getEnabled())) { + return true; + } + + return false; + } + + public static boolean isLockAcquired() { + try { + return NewRelicSecurity.isHookProcessingActive() && + Boolean.TRUE.equals(NewRelicSecurity.getAgent().getSecurityMetaData().getCustomAttribute(getNrSecCustomAttribName(), Boolean.class)); + } catch (Throwable ignored) {} + return false; + } + + public static boolean acquireLockIfPossible() { + try { + if (NewRelicSecurity.isHookProcessingActive() && + !isLockAcquired()) { + NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(getNrSecCustomAttribName(), true); + return true; + } + } catch (Throwable ignored){} + return false; + } + + public static void releaseLock() { + try { + if (NewRelicSecurity.isHookProcessingActive()) { + NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(getNrSecCustomAttribName(), null); + } + } catch (Throwable ignored) { + } + } + + private static String getNrSecCustomAttribName() { + return NR_SEC_CUSTOM_ATTRIB_NAME + Thread.currentThread().getId(); + } + + public static AbstractOperation preprocessSecurityHook(String url, String className, String methodName) { + try { + if (!NewRelicSecurity.isHookProcessingActive() || + NewRelicSecurity.getAgent().getSecurityMetaData().getRequest().isEmpty() || + url == null || url.trim().isEmpty()) { + return null; + } + + SSRFOperation operation = new SSRFOperation(url, + className, methodName); + NewRelicSecurity.getAgent().registerOperation(operation); + return operation; + } catch (Throwable e) { + if (e instanceof NewRelicSecurityException) { + e.printStackTrace(); + throw e; + } + } + return null; + } + + public static void registerExitOperation(boolean isProcessingAllowed, AbstractOperation operation) { + try { + if (operation == null || !isProcessingAllowed || !NewRelicSecurity.isHookProcessingActive() || + NewRelicSecurity.getAgent().getSecurityMetaData().getRequest().isEmpty() || skipExistsEvent() + ) { + return; + } + NewRelicSecurity.getAgent().registerExitEvent(operation); + } catch (Throwable ignored) { + } + } + + public static Request addSecurityHeaders(Request request, AbstractOperation operation) { + if (operation == null || request == null) { + return null; + } + + // Add Security IAST header + String iastHeader = NewRelicSecurity.getAgent().getSecurityMetaData().getFuzzRequestIdentifier().getRaw(); + if (iastHeader != null && !iastHeader.trim().isEmpty()) { + request.getHeaders().add(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID, iastHeader); + } + + String csecParaentId = NewRelicSecurity.getAgent().getSecurityMetaData().getCustomAttribute(GenericHelper.CSEC_PARENT_ID, String.class); + if(StringUtils.isNotBlank(csecParaentId)){ + request.getHeaders().add(GenericHelper.CSEC_PARENT_ID, csecParaentId); + } + + if (operation.getApiID() != null && !operation.getApiID().trim().isEmpty() && + operation.getExecutionId() != null && !operation.getExecutionId().trim().isEmpty()) { + // Add Security distributed tracing header + request.getHeaders().remove(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER); + request.getHeaders().add(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER, + SSRFUtils.generateTracingHeaderValue(NewRelicSecurity.getAgent().getSecurityMetaData() + .getTracingHeaderValue(), + operation.getApiID(), operation.getExecutionId(), + NewRelicSecurity.getAgent().getAgentUUID())); + } + return request; + } +} diff --git a/instrumentation-security/async-http-client-2.0.0/src/main/java/org/asynchttpclient/AsyncHttpClient_Instrumentation.java b/instrumentation-security/async-http-client-2.0.0/src/main/java/org/asynchttpclient/AsyncHttpClient_Instrumentation.java new file mode 100644 index 000000000..d8e83cd85 --- /dev/null +++ b/instrumentation-security/async-http-client-2.0.0/src/main/java/org/asynchttpclient/AsyncHttpClient_Instrumentation.java @@ -0,0 +1,60 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.asynchttpclient; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import com.newrelic.agent.security.instrumentation.org.asynchttpclient.AsynchttpHelper; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; + +/** + * Instrumentation for the provider interface. + */ +@Weave(type = MatchType.Interface, originalName = "org.asynchttpclient.AsyncHttpClient") +public abstract class AsyncHttpClient_Instrumentation { + + public ListenableFuture executeRequest(Request request, AsyncHandler handler) { + URI uri = null; + boolean isLockAcquired = AsynchttpHelper.acquireLockIfPossible(); + AbstractOperation operation = null; + if(isLockAcquired) { + try { + uri = new URI(request.getUrl()); + String scheme = uri.getScheme().toLowerCase(); + + // only instrument HTTP or HTTPS calls + if (("http".equals(scheme) || "https".equals(scheme))) { + operation = AsynchttpHelper.preprocessSecurityHook(uri.toURL().toString(), this.getClass().getName(), + AsynchttpHelper.METHOD_EXECUTE); + Request updatedRequest = AsynchttpHelper.addSecurityHeaders(request, operation); + if (updatedRequest != null) { + request = updatedRequest; + } + } + + } catch (URISyntaxException | MalformedURLException uriSyntaxException) { + // if Java can't parse the URI, asynchttpclient won't be able to either + // let's just proceed without instrumentation + } + } + ListenableFuture returnVal = null; + try { + returnVal = Weaver.callOriginal(); + } finally { + if(isLockAcquired){ + AsynchttpHelper.releaseLock(); + } + } + AsynchttpHelper.registerExitOperation(isLockAcquired, operation); + return returnVal; + } +} diff --git a/instrumentation-security/async-http-client-2.0.0/src/main/java/play/CorePlugin.java b/instrumentation-security/async-http-client-2.0.0/src/main/java/play/CorePlugin.java new file mode 100644 index 000000000..b45f88d78 --- /dev/null +++ b/instrumentation-security/async-http-client-2.0.0/src/main/java/play/CorePlugin.java @@ -0,0 +1,18 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package play; + +import com.newrelic.api.agent.weaver.SkipIfPresent; + +/** + * Play v1 instrumentation is implemented using its own set of pointcuts that don't work well with our async APIs. This + * class is present in Play v1 but not v2, and will cause this module NOT to load if the customer is using Play v1. + */ +@SkipIfPresent +public class CorePlugin { +} diff --git a/instrumentation-security/async-http-client-2.0.0/src/test/java/com/nr/agent/security/instrumentation/asynchttpclient210/AsyncHttpClientTest.java b/instrumentation-security/async-http-client-2.0.0/src/test/java/com/nr/agent/security/instrumentation/asynchttpclient210/AsyncHttpClientTest.java new file mode 100644 index 000000000..80a16881a --- /dev/null +++ b/instrumentation-security/async-http-client-2.0.0/src/test/java/com/nr/agent/security/instrumentation/asynchttpclient210/AsyncHttpClientTest.java @@ -0,0 +1,557 @@ +package com.nr.agent.security.instrumentation.asynchttpclient210; + +import com.newrelic.agent.security.introspec.InstrumentationTestConfig; +import com.newrelic.agent.security.introspec.SecurityInstrumentationTestRunner; +import com.newrelic.agent.security.introspec.SecurityIntrospector; +import com.newrelic.agent.security.introspec.internal.HttpServerRule; +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; +import com.newrelic.api.agent.security.schema.operation.SSRFOperation; +import com.newrelic.security.test.marker.Java11IncompatibleTest; +import com.newrelic.security.test.marker.Java17IncompatibleTest; +import com.newrelic.agent.security.instrumentation.org.asynchttpclient.AsynchttpHelper; +import org.asynchttpclient.*; +import org.junit.Assert; +import org.junit.FixMethodOrder; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@Category({ Java11IncompatibleTest.class, Java17IncompatibleTest.class }) +@RunWith(SecurityInstrumentationTestRunner.class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@InstrumentationTestConfig(includePrefixes = {"org.asynchttpclient", "com.newrelic.agent.security.instrumentation.org.asynchttpclient"}) +public class AsyncHttpClientTest { + + @Rule + public HttpServerRule server = new HttpServerRule(); + + @Test + public void testExecuteGet() throws URISyntaxException, IOException { + String headerValue = String.valueOf(UUID.randomUUID()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + + makeAsyncRequestGet(server.getEndPoint().toURL().toString()); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + assertEquals("Invalid number of operations detected", 1, operations.size()); + SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", AsynchttpHelper.METHOD_EXECUTE, operation.getMethodName()); + verifyHeaders(headerValue, headers); + } + + @Test + public void testExecutePost() throws URISyntaxException, IOException { + String headerValue = String.valueOf(UUID.randomUUID()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + + makeAsyncRequestPost(server.getEndPoint().toURL().toString()); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + assertEquals("Invalid number of operations detected", 1, operations.size()); + SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", AsynchttpHelper.METHOD_EXECUTE, operation.getMethodName()); + verifyHeaders(headerValue, headers); + } + + @Test + public void testExecutePut() throws URISyntaxException, IOException { + String headerValue = String.valueOf(UUID.randomUUID()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + + makeAsyncRequestPut(server.getEndPoint().toURL().toString()); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + assertEquals("Invalid number of operations detected", 1, operations.size()); + SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", AsynchttpHelper.METHOD_EXECUTE, operation.getMethodName()); + verifyHeaders(headerValue, headers); + } + + @Test + public void testExecuteDelete() throws URISyntaxException, IOException { + String headerValue = String.valueOf(UUID.randomUUID()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + + makeAsyncRequestDelete(server.getEndPoint().toURL().toString()); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + assertEquals("Invalid number of operations detected", 1, operations.size()); + SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", AsynchttpHelper.METHOD_EXECUTE, operation.getMethodName()); + verifyHeaders(headerValue, headers); + } + + @Test + public void testExecuteHead() throws URISyntaxException, IOException { + String headerValue = String.valueOf(UUID.randomUUID()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + + makeAsyncRequestHead(server.getEndPoint().toURL().toString()); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + assertEquals("Invalid number of operations detected", 1, operations.size()); + SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", AsynchttpHelper.METHOD_EXECUTE, operation.getMethodName()); + verifyHeaders(headerValue, headers); + } + + @Test + public void testExecuteOptions() throws URISyntaxException, IOException { + String headerValue = String.valueOf(UUID.randomUUID()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + + makeAsyncRequestOptions(server.getEndPoint().toURL().toString()); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + assertEquals("Invalid number of operations detected", 1, operations.size()); + SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", AsynchttpHelper.METHOD_EXECUTE, operation.getMethodName()); + verifyHeaders(headerValue, headers); + } + + @Test + public void testExecuteConnect() throws URISyntaxException, IOException, InterruptedException { + String headerValue = String.valueOf(UUID.randomUUID()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + server.getHeaders().clear(); + makeAsyncRequestConnect(server.getEndPoint().toURL().toString()); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + assertEquals("Invalid number of operations detected", 1, operations.size()); + SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", AsynchttpHelper.METHOD_EXECUTE, operation.getMethodName()); + Assert.assertFalse(String.format("Unexpected K2 header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); + Assert.assertFalse(String.format("Unexpected K2 header: %s", GenericHelper.CSEC_PARENT_ID), headers.containsKey(GenericHelper.CSEC_PARENT_ID)); + Assert.assertFalse(String.format("Unexpected K2 header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + } + + @Test + public void testExecuteRequest() throws URISyntaxException, IOException { + String headerValue = String.valueOf(UUID.randomUUID()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + + makeAsyncRequest(server.getEndPoint().toURL().toString()); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + assertEquals("Invalid number of operations detected", 1, operations.size()); + SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", AsynchttpHelper.METHOD_EXECUTE, operation.getMethodName()); + verifyHeaders(headerValue, headers); + } + + @Test + public void testExecuteRequestBuilder() throws URISyntaxException, IOException { + String headerValue = String.valueOf(UUID.randomUUID()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + + makeAsyncRequestBuilder(server.getEndPoint().toURL().toString()); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + assertEquals("Invalid number of operations detected", 1, operations.size()); + SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", AsynchttpHelper.METHOD_EXECUTE, operation.getMethodName()); + verifyHeaders(headerValue, headers); + } + + @Test + public void testExecuteTrace() throws URISyntaxException, IOException { + String headerValue = String.valueOf(UUID.randomUUID()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + + makeAsyncRequestTrace(server.getEndPoint().toURL().toString()); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + assertEquals("Invalid number of operations detected", 1, operations.size()); + SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", AsynchttpHelper.METHOD_EXECUTE, operation.getMethodName()); + verifyHeaders(headerValue, headers); + } + + @Test + public void testExecute1() throws URISyntaxException, IOException { + String headerValue = String.valueOf(UUID.randomUUID()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + + makeAsyncRequest1(server.getEndPoint().toURL().toString()); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + assertEquals("Invalid number of operations detected", 1, operations.size()); + SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", AsynchttpHelper.METHOD_EXECUTE, operation.getMethodName()); + verifyHeaders(headerValue, headers); + } + + @Test + public void testExecute2() throws URISyntaxException, IOException { + String headerValue = String.valueOf(UUID.randomUUID()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + + makeAsyncRequest2(server.getEndPoint().toURL().toString()); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + assertEquals("Invalid number of operations detected", 1, operations.size()); + SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", AsynchttpHelper.METHOD_EXECUTE, operation.getMethodName()); + verifyHeaders(headerValue, headers); + } + + @Test + public void testExecute3() throws URISyntaxException, IOException { + String headerValue = String.valueOf(UUID.randomUUID()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + + makeAsyncRequest3(server.getEndPoint().toURL().toString()); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + assertEquals("Invalid number of operations detected", 1, operations.size()); + SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", AsynchttpHelper.METHOD_EXECUTE, operation.getMethodName()); + verifyHeaders(headerValue, headers); + } + + @Test + public void testExecute4() throws URISyntaxException, IOException { + String headerValue = String.valueOf(UUID.randomUUID()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + + makeAsyncRequest4(server.getEndPoint().toURL().toString()); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + assertEquals("Invalid number of operations detected", 1, operations.size()); + SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", AsynchttpHelper.METHOD_EXECUTE, operation.getMethodName()); + verifyHeaders(headerValue, headers); + } + + @Trace(dispatcher = true) + private static void makeAsyncRequestGet(String url) { + DefaultAsyncHttpClientConfig.Builder clientBuilder = Dsl.config() + .setConnectTimeout(500); + + try (AsyncHttpClient client = Dsl.asyncHttpClient(clientBuilder)) { + BoundRequestBuilder builder = client.prepareGet(url); + Future future = builder.execute(); + Response response = future.get(); + response.getStatusCode(); + } catch (Exception e) { + } + } + + @Trace(dispatcher = true) + private static void makeAsyncRequestPost(String url) { + DefaultAsyncHttpClientConfig.Builder clientBuilder = Dsl.config() + .setConnectTimeout(500); + + try (AsyncHttpClient client = Dsl.asyncHttpClient(clientBuilder)) { + BoundRequestBuilder builder = client.preparePost(url); + Future future = builder.execute(); + Response response = future.get(); + response.getStatusCode(); + } catch (Exception e) { + } + } + + @Trace(dispatcher = true) + private static void makeAsyncRequestPut(String url) { + DefaultAsyncHttpClientConfig.Builder clientBuilder = Dsl.config() + .setConnectTimeout(500); + + try (AsyncHttpClient client = Dsl.asyncHttpClient(clientBuilder)) { + BoundRequestBuilder builder = client.preparePut(url); + Future future = builder.execute(); + Response response = future.get(); + response.getStatusCode(); + } catch (Exception e) { + } + } + + @Trace(dispatcher = true) + private static void makeAsyncRequestDelete(String url) { + DefaultAsyncHttpClientConfig.Builder clientBuilder = Dsl.config() + .setConnectTimeout(500); + + try (AsyncHttpClient client = Dsl.asyncHttpClient(clientBuilder)) { + BoundRequestBuilder builder = client.prepareDelete(url); + Future future = builder.execute(); + Response response = future.get(); + response.getStatusCode(); + } catch (Exception e) { + } + } + + @Trace(dispatcher = true) + private static void makeAsyncRequestHead(String url) { + DefaultAsyncHttpClientConfig.Builder clientBuilder = Dsl.config() + .setConnectTimeout(500); + + try (AsyncHttpClient client = Dsl.asyncHttpClient(clientBuilder)) { + BoundRequestBuilder builder = client.prepareHead(url); + Future future = builder.execute(); + Response response = future.get(); + response.getStatusCode(); + } catch (Exception e) { + } + } + + @Trace(dispatcher = true) + private static void makeAsyncRequestOptions(String url) { + DefaultAsyncHttpClientConfig.Builder clientBuilder = Dsl.config() + .setConnectTimeout(500); + + try (AsyncHttpClient client = Dsl.asyncHttpClient(clientBuilder)) { + BoundRequestBuilder builder = client.prepareOptions(url); + Future future = builder.execute(); + Response response = future.get(); + response.getStatusCode(); + } catch (Exception e) { + } + } + + @Trace(dispatcher = true) + private static void makeAsyncRequestConnect(String url) { + DefaultAsyncHttpClientConfig.Builder clientBuilder = Dsl.config() + .setConnectTimeout(500); + + try (AsyncHttpClient client = Dsl.asyncHttpClient(clientBuilder)) { + BoundRequestBuilder builder = client.prepareConnect(url); + Future future = builder.execute(); +// future.get(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Trace(dispatcher = true) + private static void makeAsyncRequest(String url) { + DefaultAsyncHttpClientConfig.Builder clientBuilder = Dsl.config() + .setConnectTimeout(500); + + try (AsyncHttpClient client = Dsl.asyncHttpClient(clientBuilder)) { + RequestBuilder requestBuilder = new RequestBuilder(); + requestBuilder.setUrl(url); + BoundRequestBuilder builder = client.prepareRequest(requestBuilder.build()); + Future future = builder.execute(); + Response response = future.get(); + response.getStatusCode(); + } catch (Exception e) { + } + } + + @Trace(dispatcher = true) + private static void makeAsyncRequestBuilder(String url) { + DefaultAsyncHttpClientConfig.Builder clientBuilder = Dsl.config() + .setConnectTimeout(500); + + try (AsyncHttpClient client = Dsl.asyncHttpClient(clientBuilder)) { + RequestBuilder requestBuilder = new RequestBuilder(); + requestBuilder.setUrl(url); + BoundRequestBuilder builder = client.prepareRequest(requestBuilder); + Future future = builder.execute(); + Response response = future.get(); + response.getStatusCode(); + } catch (Exception e) { + } + } + + @Trace(dispatcher = true) + private static void makeAsyncRequestTrace(String url) { + DefaultAsyncHttpClientConfig.Builder clientBuilder = Dsl.config() + .setConnectTimeout(500); + + try (AsyncHttpClient client = Dsl.asyncHttpClient(clientBuilder)) { + BoundRequestBuilder builder = client.prepareTrace(url); + Future future = builder.execute(); + Response response = future.get(); + response.getStatusCode(); + } catch (Exception e) { + } + } + + @Trace(dispatcher = true) + private static void makeAsyncRequest1(String url) { + try(AsyncHttpClient client = new DefaultAsyncHttpClient()){ + RequestBuilder requestBuilder = new RequestBuilder(); + requestBuilder.setUrl(url); + ListenableFuture future = client.executeRequest(requestBuilder.build()); + Response response = null; + response = future.get(); + response.getStatusCode(); + } catch (InterruptedException | ExecutionException | IOException e) { + } + } + + @Trace(dispatcher = true) + private static void makeAsyncRequest2(String url) { + try(AsyncHttpClient client = new DefaultAsyncHttpClient(new DefaultAsyncHttpClientConfig.Builder().build())){ + RequestBuilder requestBuilder = new RequestBuilder(); + requestBuilder.setUrl(url); + ListenableFuture future = client.executeRequest(requestBuilder); + Response response = null; + response = future.get(); + response.getStatusCode(); + } catch (InterruptedException | ExecutionException | IOException e) { + } + } + + @Trace(dispatcher = true) + private static void makeAsyncRequest3(String url) { + try(AsyncHttpClient client = new DefaultAsyncHttpClient(new DefaultAsyncHttpClientConfig.Builder().build())){ + RequestBuilder requestBuilder = new RequestBuilder(); + requestBuilder.setUrl(url); + Future f = client.executeRequest(requestBuilder.build(), new AsyncCompletionHandler() { + + @Override + public Response onCompleted(Response response) throws IOException { + return response; + } + + @Override + public void onThrowable(Throwable t) { + } + }); + Response response = f.get(); + response.getStatusCode(); + } catch (InterruptedException | ExecutionException | IOException e) { + } + } + + @Trace(dispatcher = true) + private static void makeAsyncRequest4(String url) { + try(AsyncHttpClient client = new DefaultAsyncHttpClient(new DefaultAsyncHttpClientConfig.Builder().build())){ + RequestBuilder requestBuilder = new RequestBuilder(); + requestBuilder.setUrl(url); + Future f = client.executeRequest(requestBuilder, new AsyncCompletionHandler() { + + @Override + public Response onCompleted(Response response) throws IOException { + return response; + } + + @Override + public void onThrowable(Throwable t) { + } + }); + Response response = f.get(); + response.getStatusCode(); + } catch (InterruptedException | ExecutionException | IOException e) { + } + } + + private void setCSECHeaders(String headerValue, SecurityIntrospector introspector) { + introspector.setK2FuzzRequestId(headerValue+"a"); + introspector.setK2ParentId(headerValue+"b"); + introspector.setK2TracingData(headerValue); + } + + private void verifyHeaders(String headerValue, Map headers) { + Assert.assertTrue(String.format("Missing K2 header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue+"a", headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); + Assert.assertTrue(String.format("Missing K2 header: %s", GenericHelper.CSEC_PARENT_ID), headers.containsKey(GenericHelper.CSEC_PARENT_ID)); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", GenericHelper.CSEC_PARENT_ID), headerValue+"b", headers.get(GenericHelper.CSEC_PARENT_ID)); + Assert.assertTrue(String.format("Missing K2 header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", + headerValue), headers.get( + ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + } +} diff --git a/instrumentation-security/cassandra-datastax-3/.gitignore b/instrumentation-security/cassandra-datastax-3/.gitignore new file mode 100644 index 000000000..2f7896d1d --- /dev/null +++ b/instrumentation-security/cassandra-datastax-3/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/instrumentation-security/cassandra-datastax-3/build.gradle b/instrumentation-security/cassandra-datastax-3/build.gradle new file mode 100644 index 000000000..c520cbf9a --- /dev/null +++ b/instrumentation-security/cassandra-datastax-3/build.gradle @@ -0,0 +1,24 @@ +dependencies { + implementation(project(":newrelic-security-api")) + implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}") + implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}") + implementation("com.datastax.cassandra:cassandra-driver-core:3.2.0") + + testImplementation("org.cassandraunit:cassandra-unit:3.1.1.0") + testImplementation("com.github.jbellis:jamm:0.3.2") +} + +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.cassandra-datastax-3' } +} + +verifyInstrumentation { + passesOnly 'com.datastax.cassandra:cassandra-driver-core:[3.0.0,4.0.0)' + excludeRegex ".*(rc|beta|alpha).*" + excludeRegex('com.datastax.cassandra:cassandra-driver-core:2.*') +} + +site { + title 'Cassandra' + type 'Datastore' +} \ No newline at end of file diff --git a/instrumentation-security/cassandra-datastax-3/src/main/java/com/datastax/driver/core/SessionManager_Instrumentation.java b/instrumentation-security/cassandra-datastax-3/src/main/java/com/datastax/driver/core/SessionManager_Instrumentation.java new file mode 100644 index 000000000..4bbaec148 --- /dev/null +++ b/instrumentation-security/cassandra-datastax-3/src/main/java/com/datastax/driver/core/SessionManager_Instrumentation.java @@ -0,0 +1,39 @@ +package com.datastax.driver.core; + +import com.newrelic.agent.security.instrumentation.cassandra3.CassandraUtils; +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; + +@Weave(type = MatchType.ExactClass, originalName = "com.datastax.driver.core.SessionManager") +abstract class SessionManager_Instrumentation { + abstract Configuration configuration(); + + public ResultSetFuture executeAsync(Statement statement) { + boolean isLockAcquired = CassandraUtils.acquireLockIfPossible(statement.hashCode()); + ResultSetFuture result; + AbstractOperation cqlOperation = null; + + try { + result = Weaver.callOriginal(); + if(statement instanceof StatementWrapper){ + statement = ((StatementWrapper) statement).getWrappedStatement(); + } + + if(isLockAcquired){ + cqlOperation = CassandraUtils.preProcessSecurityHook(statement, configuration(), this.getClass().getName()); + if(cqlOperation != null){ + NewRelicSecurity.getAgent().registerOperation(cqlOperation); + } + } + } finally { + if(isLockAcquired){ + CassandraUtils.releaseLock(statement.hashCode()); + } + } + CassandraUtils.registerExitOperation(isLockAcquired, cqlOperation); + return result; + } +} \ No newline at end of file diff --git a/instrumentation-security/cassandra-datastax-3/src/main/java/com/datastax/driver/core/SimpleStatement_Instrumentation.java b/instrumentation-security/cassandra-datastax-3/src/main/java/com/datastax/driver/core/SimpleStatement_Instrumentation.java new file mode 100644 index 000000000..81cf8a05f --- /dev/null +++ b/instrumentation-security/cassandra-datastax-3/src/main/java/com/datastax/driver/core/SimpleStatement_Instrumentation.java @@ -0,0 +1,76 @@ +package com.datastax.driver.core; + +import com.newrelic.agent.security.instrumentation.cassandra3.CassandraUtils; +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; +import com.newrelic.api.agent.security.schema.operation.SQLOperation; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; + +@Weave(type= MatchType.ExactClass, originalName = "com.datastax.driver.core.SimpleStatement") +public abstract class SimpleStatement_Instrumentation { + + public SimpleStatement_Instrumentation(String query, Object... values) { + boolean isLockAcquired = CassandraUtils.acquireLockIfPossible(hashCode()); + + try{ + if(isLockAcquired){ + SQLOperation cqlOperation = new SQLOperation(this.getClass().getName(), CassandraUtils.METHOD_EXECUTE_ASYNC); + cqlOperation.setQuery(query); + cqlOperation.setCaseType(VulnerabilityCaseType.NOSQL_DB_COMMAND); + cqlOperation.setDbName(CassandraUtils.EVENT_CATEGORY); + if (values != null){ + cqlOperation.setParams(setParams(values)); + } + NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute( + CassandraUtils.NR_SEC_CUSTOM_ATTRIB_CQL_STMT + hashCode(), cqlOperation); + } + } finally { + if(isLockAcquired){ + CassandraUtils.releaseLock(hashCode()); + } + } + } + + public SimpleStatement_Instrumentation(String query, Map values){ + boolean isLockAcquired = CassandraUtils.acquireLockIfPossible(hashCode()); + + try{ + if(isLockAcquired){ + SQLOperation cqlOperation = new SQLOperation(this.getClass().getName(), CassandraUtils.METHOD_EXECUTE_ASYNC); + cqlOperation.setQuery(query); + cqlOperation.setCaseType(VulnerabilityCaseType.NOSQL_DB_COMMAND); + cqlOperation.setDbName(CassandraUtils.EVENT_CATEGORY); + cqlOperation.setParams(setParams(values)); + NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute( + CassandraUtils.NR_SEC_CUSTOM_ATTRIB_CQL_STMT + hashCode(), cqlOperation); + } + } finally { + if(isLockAcquired){ + CassandraUtils.releaseLock(hashCode()); + } + } + } + private Map setParams(Object... values) { + Map params = new HashMap<>(); + for(int i = 0; i < values.length; i++){ + if(!(values[i] instanceof ByteBuffer)){ + params.put(String.valueOf(i), String.valueOf(values[i])); + } + } + return params; + } + private Map setParams(Map values) { + Map params = new HashMap<>(); + for( Map.Entry namedVal: values.entrySet()) { + if(!(namedVal.getValue() instanceof ByteBuffer)){ + params.put(namedVal.getKey(), String.valueOf(namedVal.getValue())); + } + } + return params; + } +} diff --git a/instrumentation-security/cassandra-datastax-3/src/main/java/com/newrelic/agent/security/instrumentation/cassandra3/CassandraUtils.java b/instrumentation-security/cassandra-datastax-3/src/main/java/com/newrelic/agent/security/instrumentation/cassandra3/CassandraUtils.java new file mode 100644 index 000000000..e8f9d3177 --- /dev/null +++ b/instrumentation-security/cassandra-datastax-3/src/main/java/com/newrelic/agent/security/instrumentation/cassandra3/CassandraUtils.java @@ -0,0 +1,134 @@ +package com.newrelic.agent.security.instrumentation.cassandra3; + +import com.datastax.driver.core.BatchStatement; +import com.datastax.driver.core.BoundStatement; +import com.datastax.driver.core.CodecRegistry; +import com.datastax.driver.core.ColumnDefinitions; +import com.datastax.driver.core.Configuration; +import com.datastax.driver.core.ProtocolVersion; +import com.datastax.driver.core.Statement; +import com.datastax.driver.core.TypeCodec; +import com.datastax.driver.core.querybuilder.BuiltStatement; +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; +import com.newrelic.api.agent.security.schema.operation.BatchSQLOperation; +import com.newrelic.api.agent.security.schema.operation.SQLOperation; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class CassandraUtils { + public static final String METHOD_EXECUTE_ASYNC = "executeAsync"; + public static final String NR_SEC_CUSTOM_ATTRIB_CQL_STMT = "NR-CQL-STMT"; + public static final String EVENT_CATEGORY = "CQL"; + public static final String NR_SEC_CASSANDRA_LOCK = "CASSANDRA_OPERATION_LOCK"; + public static boolean acquireLockIfPossible(int hashcode) { + try { + return GenericHelper.acquireLockIfPossible(NR_SEC_CASSANDRA_LOCK + hashcode); + } catch (Exception ignored){ + } + return false; + } + + public static AbstractOperation preProcessSecurityHook(Statement statement, Configuration config, String klass) { + try { + SQLOperation cqlOperation = new SQLOperation(klass, CassandraUtils.METHOD_EXECUTE_ASYNC); + cqlOperation.setCaseType(VulnerabilityCaseType.NOSQL_DB_COMMAND); + cqlOperation.setDbName(EVENT_CATEGORY); + + if (statement instanceof BatchStatement){ + BatchSQLOperation batchCQLOperation = new BatchSQLOperation(klass, METHOD_EXECUTE_ASYNC); + batchCQLOperation.setCaseType(VulnerabilityCaseType.NOSQL_DB_COMMAND); + + for (Statement stmt: ((BatchStatement) statement).getStatements()) { + AbstractOperation operation = preProcessSecurityHook(stmt, config, klass); + if (operation instanceof SQLOperation) + batchCQLOperation.addOperation((SQLOperation) operation); + } + + return batchCQLOperation; + } else if(statement instanceof BuiltStatement){ + BuiltStatement stmt = (BuiltStatement) statement; + cqlOperation.setQuery(stmt.getQueryString()); + cqlOperation.setParams(setParams(stmt, config.getProtocolOptions().getProtocolVersion(), config.getCodecRegistry())); + return cqlOperation; + + } else if (statement instanceof BoundStatement) { + BoundStatement stmt = (BoundStatement) statement; + cqlOperation.setQuery(stmt.preparedStatement().getQueryString()); + cqlOperation.setParams(setParams(stmt)); + return cqlOperation; + + } else { + return NewRelicSecurity.getAgent().getSecurityMetaData().getCustomAttribute( + NR_SEC_CUSTOM_ATTRIB_CQL_STMT+statement.hashCode(), SQLOperation.class); + } + } catch (Exception ignored) { + } + return null; + } + + public static void registerExitOperation(boolean isProcessingAllowed, AbstractOperation operation) { + try { + if(operation == null || !isProcessingAllowed || !NewRelicSecurity.isHookProcessingActive() || + NewRelicSecurity.getAgent().getSecurityMetaData().getRequest().isEmpty() || GenericHelper.skipExistsEvent() + ){ + return; + } + if(operation instanceof SQLOperation){ + SQLOperation cqlOp = (SQLOperation) operation; + if(cqlOp.getQuery().isEmpty() || cqlOp.getQuery().trim().isEmpty()) { + return; + } + } + NewRelicSecurity.getAgent().registerExitEvent(operation); + } catch (Exception ignored) { + } + } + + private static Map setParams(BuiltStatement statement, ProtocolVersion protoVersion, CodecRegistry registry) { + Map params = new HashMap<>(); + try{ + if(statement.hasValues()){ + for(int i = 0; i < statement.getValues(protoVersion, registry).length; i++){ + Object obj; + if(!((obj = statement.getObject(i, registry)) instanceof ByteBuffer)){ + params.put(String.valueOf(i), String.valueOf(obj)); + } + } + } + } catch (Exception ignored){ + } + return params; + } + + public static Map setParams(BoundStatement statement) { + Map params = new HashMap<>(); + List variables = statement.preparedStatement().getVariables().asList(); + try{ + for (int i = 0; i < variables.size(); i++) { + ColumnDefinitions.Definition variable = variables.get(i); + CodecRegistry codecRegistry = statement.preparedStatement().getCodecRegistry(); + TypeCodec codec = codecRegistry.codecFor(variable.getType()); + Object value = statement.get(variable.getName(), codec); + + if (!(value instanceof ByteBuffer)) { + params.put(String.valueOf(i), String.valueOf(value)); + } + } + } catch (Exception ignored){ + } + return params; + } + + public static void releaseLock(int hashcode) { + try { + GenericHelper.releaseLock(NR_SEC_CASSANDRA_LOCK + hashcode); + } catch (Throwable ignored) { + } + } +} diff --git a/instrumentation-security/cassandra-datastax-3/src/test/java/com/nr/agent/security/instrumentation/cassandra3/CassandraTest.java b/instrumentation-security/cassandra-datastax-3/src/test/java/com/nr/agent/security/instrumentation/cassandra3/CassandraTest.java new file mode 100644 index 000000000..7d96f9dca --- /dev/null +++ b/instrumentation-security/cassandra-datastax-3/src/test/java/com/nr/agent/security/instrumentation/cassandra3/CassandraTest.java @@ -0,0 +1,675 @@ +package com.nr.agent.security.instrumentation.cassandra3; + +import com.datastax.driver.core.BatchStatement; +import com.datastax.driver.core.BoundStatement; +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.CodecRegistry; +import com.datastax.driver.core.LocalDate; +import com.datastax.driver.core.Session; +import com.datastax.driver.core.SimpleStatement; +import com.datastax.driver.core.TypeCodec; +import com.datastax.driver.core.querybuilder.Batch; +import com.newrelic.agent.security.instrumentation.cassandra3.CassandraUtils; +import com.datastax.driver.core.querybuilder.Delete; +import com.datastax.driver.core.querybuilder.Insert; +import com.datastax.driver.core.querybuilder.QueryBuilder; +import com.datastax.driver.core.querybuilder.Select; +import com.datastax.driver.core.querybuilder.Update; +import com.google.common.collect.ImmutableMap; +import com.newrelic.agent.security.introspec.InstrumentationTestConfig; +import com.newrelic.agent.security.introspec.SecurityInstrumentationTestRunner; +import com.newrelic.agent.security.introspec.SecurityIntrospector; +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; +import com.newrelic.api.agent.security.schema.operation.BatchSQLOperation; +import com.newrelic.api.agent.security.schema.operation.SQLOperation; +import com.newrelic.security.test.marker.Java11IncompatibleTest; +import com.newrelic.security.test.marker.Java17IncompatibleTest; +import com.newrelic.security.test.marker.Java9IncompatibleTest; +import org.apache.cassandra.io.util.FileUtils; +import org.cassandraunit.utils.EmbeddedCassandraServerHelper; +import org.joda.time.DateTime; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; + +import java.io.File; +import java.math.BigDecimal; +import java.net.URL; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +// Issue when running cassandra unit on Java 9+ - https://github.com/jsevellec/cassandra-unit/issues/249 +@RunWith(SecurityInstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = { "com.datastax.driver.core", "com.newrelic.agent.security.instrumentation.cassandra3" }) +@Category({ Java9IncompatibleTest.class, Java11IncompatibleTest.class, Java17IncompatibleTest.class }) +public class CassandraTest { + private static Cluster CLUSTER; + private static Session SESSION; + private static final List QUERIES = CassandraTestUtils.getQueries(); + private static int PORT; + + @BeforeClass + public static void beforeClass() throws Exception { + /* Embedded Cassandra doesn't play nice in java9 - when you attempt to start it, it attempts to create and initialize + a directory on the local file system. It uses its own FileUtils class do so, which contains a static + initialization block that tries to cast a ByteBuffer to a DirectBuffer, which doesn't exist in Java 9. This falls + through to a catch block, which subsequently calls JVMStabilityInspector.inspectThrowable(t), which in turn + calls DatabaseDescriptor.getDiskFailurePolicy(), and that, in turn, relies on the directory having been created. + */ + System.setProperty("java.library.path","src/test/resources/libs/"); + URL config = CassandraTest.class.getResource("/cu-cassandra.yaml"); + System.setProperty("cassandra.config", config.toString()); + EmbeddedCassandraServerHelper.startEmbeddedCassandra(); + + PORT = EmbeddedCassandraServerHelper.getNativeTransportPort(); + CLUSTER = Cluster.builder().withPort(PORT).addContactPoint("127.0.0.1").build(); + + SESSION = CLUSTER.connect(); + + // CREATE A KEYSPACE test and use + SESSION.execute(QUERIES.get(0)); + SESSION.execute(QUERIES.get(1)); + + // create table users and users2 + SESSION.execute(QUERIES.get(2)); + SESSION.execute(QUERIES.get(3)); + } + @AfterClass + public static void after() { + if(SESSION != null){ + SESSION.closeAsync(); + } + if(CLUSTER != null){ + CLUSTER.closeAsync(); + } + FileUtils.deleteRecursive(new File("target/")); + } + @Test + public void testStringSimpleStmt() { + stringSimpleStmt(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "executeAsync", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(4), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + } + @Test + public void testSimpleStmt() { + simpleStmt(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "executeAsync", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(4), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + } + @Test + public void testSimpleStmtParams() { + Map params = simpleStmtParams(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "executeAsync", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(5), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + @Test + public void testSimpleStmtNamedParams() { + Map params = simpleStmtNamedParams(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "executeAsync", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(6), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + @Test + public void testWrappedStmt() { + wrappedStmt(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "executeAsync", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(4), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + } + @Test + public void testBoundStmt() { + Map params = boundStmt(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "executeAsync", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(7), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + @Test + public void testBuiltStmtInsert() { + Map params = builtStmtInsert(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "executeAsync", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(10), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + @Test + public void testBuiltStmtInsert1() { + Map params = builtStmtInsert1(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "executeAsync", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(16), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + @Test + public void testBuiltStmtSelect() { + Map params = builtStmtSelect(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "executeAsync", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(12), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + @Test + public void testBuiltStmtSelect1() { + Map params = builtStmtSelect1(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "executeAsync", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(15), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + @Test + public void testBuiltStmtUpdate() { + Map params = builtStmtUpdate(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "executeAsync", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(14), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + @Test + public void testBuiltStmtUpdate1() { + Map params = builtStmtUpdate1(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "executeAsync", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(18), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + @Test + public void testBuiltStmtDelete() { + Map params = builtStmtDelete(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "executeAsync", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(13), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + @Test + public void testBuiltStmtDelete1() { + Map params = builtStmtDelete1(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "executeAsync", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(17), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + @Test + public void testBuiltStmtBatch() { + Map params = builtStmtBatch(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "executeAsync", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(11), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + @Test + public void testBatchStmt() { + Map params = batchStmt(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + BatchSQLOperation batchOperation = (BatchSQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, batchOperation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "executeAsync", batchOperation.getMethodName()); + Assert.assertEquals("Wrong number of operations detected", 2, batchOperation.getOperations().size()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(4), batchOperation.getOperations().get(0).getQuery()); + Assert.assertEquals("Invalid Query detected.", QUERIES.get(5), batchOperation.getOperations().get(1).getQuery()); + + Assert.assertEquals("Wrong params detected", new HashMap<>(), batchOperation.getOperations().get(0).getParams()); + Assert.assertEquals("Wrong params detected", params, batchOperation.getOperations().get(1).getParams()); + + for (SQLOperation operation: batchOperation.getOperations()) { + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "executeAsync", operation.getMethodName()); + + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + } + } + @Test + public void testNestedBatchStmt() { + Map params = nestedBatchStmt(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + BatchSQLOperation batchOperation = (BatchSQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, batchOperation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "executeAsync", batchOperation.getMethodName()); + + Assert.assertEquals("Wrong number of operations detected", 2, batchOperation.getOperations().size()); + Assert.assertEquals("Invalid Query detected.", QUERIES.get(4), batchOperation.getOperations().get(0).getQuery()); + Assert.assertEquals("Invalid Query detected.", QUERIES.get(5), batchOperation.getOperations().get(1).getQuery()); + + Assert.assertEquals("Wrong params detected", new HashMap<>(), batchOperation.getOperations().get(0).getParams()); + Assert.assertEquals("Wrong params detected", params, batchOperation.getOperations().get(1).getParams()); + + for (SQLOperation operation: batchOperation.getOperations()) { + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "executeAsync", operation.getMethodName()); + + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + } + } + @Test + public void testBatchStmtWithBuiltStmt() { + Map params = batchStmtWithBuiltStmt(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + BatchSQLOperation batchOperation = (BatchSQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, batchOperation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "executeAsync", batchOperation.getMethodName()); + Assert.assertEquals("Wrong number of operations detected", batchOperation.getOperations().size(), 2); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(14), batchOperation.getOperations().get(0).getQuery()); + Assert.assertEquals("Invalid Query detected.", QUERIES.get(10), batchOperation.getOperations().get(1).getQuery()); + + for (SQLOperation operation: batchOperation.getOperations()) { + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "executeAsync", operation.getMethodName()); + + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + } + @Test + public void testCustomCodec() { + Map params = customCodecCase(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 1); + + SQLOperation operation = (SQLOperation) operations.get(1); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "executeAsync", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(7), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + @Test + public void testCustomCodec1() { + Map params = customCodecCase1(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 2); + + SQLOperation operation = (SQLOperation) operations.get(2); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "executeAsync", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(9), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + + @Trace(dispatcher = true) + private void stringSimpleStmt() { + SESSION.execute(QUERIES.get(4)); + } + @Trace(dispatcher = true) + private void simpleStmt() { + SimpleStatement insertStmt = new SimpleStatement(QUERIES.get(4)); + SESSION.execute(insertStmt); + } + @Trace(dispatcher = true) + private Map simpleStmtParams() { + Map params = CassandraTestUtils.getValueParams(); + + SimpleStatement insertStmt1 = new SimpleStatement( + QUERIES.get(5), + Integer.parseInt(params.get("0")), + params.get("1")); + SESSION.execute(insertStmt1); + return params; + } + @Trace(dispatcher = true) + private Map simpleStmtNamedParams() { + Map params = CassandraTestUtils.getNamedParams(); + + SimpleStatement insertStmt2 = new SimpleStatement( + QUERIES.get(6), + ImmutableMap.of("age", Integer.parseInt(params.get("age")), "email", params.get("email"))); + SESSION.execute(insertStmt2); + return params; + } + + @Trace(dispatcher = true) + private void wrappedStmt() { + SimpleStatement insertStatement = new SimpleStatement(QUERIES.get(4)); + CassandraTestUtils.SimpleStatementWrapper wrappedInsertStmt = new CassandraTestUtils.SimpleStatementWrapper(insertStatement); + SESSION.execute(wrappedInsertStmt); + } + @Trace(dispatcher = true) + private Map boundStmt() { + Map params = CassandraTestUtils.getBoundParams(); + + BoundStatement boundStmt = SESSION.prepare(QUERIES.get(7)).bind(); + boundStmt.setUUID(0, UUID.fromString(params.get("0"))); + boundStmt.set("email", params.get("1"), TypeCodec.varchar()); + boundStmt.setString("email", params.get("1")); + boundStmt.setInt(2, Integer.parseInt(params.get("2"))); + boundStmt.setBool("isMarried", Boolean.parseBoolean(params.get("3"))); + boundStmt.set("age", Integer.parseInt(params.get("2")), TypeCodec.cint()); + boundStmt.setBytes("img", ByteBuffer.wrap("data".getBytes())); + boundStmt.setDecimal("phone", new BigDecimal(22222222)); + boundStmt.set("phone", new BigDecimal(22222222), BigDecimal.class); + boundStmt.setDate("dob", LocalDate.fromDaysSinceEpoch(100)); + boundStmt.setString("name", "ishi"); + boundStmt.setList("events", new ArrayList<>()); + boundStmt.setSet("address", new HashSet<>()); + boundStmt.setMap("marks", new HashMap<>()); + SESSION.execute(boundStmt); + return params; + } + @Trace(dispatcher = true) + private Map builtStmtInsert() { + Map params = new HashMap<>(); + params.put("0", "clun5@gmail.com"); + Insert stmt1 = QueryBuilder.insertInto("users") + .value("age", 35) + .value("email", params.get("0")); + stmt1.setForceNoValues(false); + SESSION.execute(stmt1); + return params; + } + + @Trace(dispatcher = true) + private Map builtStmtInsert1() { + Insert stmt1 = QueryBuilder.insertInto("users") + .value("age", 35) + .value("email", "clun5@gmail.com"); + stmt1.setForceNoValues(true); + SESSION.execute(stmt1); + return new HashMap<>(); + } + + @Trace(dispatcher = true) + private Map builtStmtSelect() { + Map params = new HashMap<>(); + params.put("0", "clun5@gmail.com"); + Select.Where stmt2 = QueryBuilder.select().all().from("users") + .where(QueryBuilder.eq("email", params.get("0"))); + stmt2.setForceNoValues(false); + SESSION.execute(stmt2); + return params; + } + + @Trace(dispatcher = true) + private Map builtStmtSelect1() { + Select.Where stmt2 = QueryBuilder.select().all().from("users") + .where(QueryBuilder.eq("email", "clun5@gmail.com")); + stmt2.setForceNoValues(true); + SESSION.execute(stmt2); + return new HashMap<>(); + } + @Trace(dispatcher = true) + private Map builtStmtDelete() { + Map params = new HashMap<>(); + params.put("0", "clun5@gmail.com"); + Delete.Where stmt3= QueryBuilder.delete().all().from("users"). + where(QueryBuilder.eq("email", params.get("0"))); + stmt3.setForceNoValues(false); + SESSION.execute(stmt3); + return params; + } + + @Trace(dispatcher = true) + private Map builtStmtDelete1() { + Delete.Where stmt3= QueryBuilder.delete().all().from("users"). + where(QueryBuilder.eq("email", "clun5@gmail.com")); + stmt3.setForceNoValues(true); + SESSION.execute(stmt3); + return new HashMap<>(); + } + @Trace(dispatcher = true) + private Map builtStmtUpdate() { + Map params = new HashMap<>(); + params.put("0", "clun5@gmail.com"); + Update.Where stmt4= QueryBuilder.update("users") + .with(QueryBuilder.set("age", 50)) + .where(QueryBuilder.eq("email", params.get("0"))); + + SESSION.execute(stmt4); + return params; + } + @Trace(dispatcher = true) + private Map builtStmtUpdate1() { + Update.Where stmt4= QueryBuilder.update("users") + .with(QueryBuilder.set("age", 50)) + .where(QueryBuilder.eq("email", "clun5@gmail.com")); + stmt4.setForceNoValues(true); + SESSION.execute(stmt4); + return new HashMap<>(); + } + @Trace(dispatcher = true) + private Map builtStmtBatch() { + Map params = new HashMap<>(); + params.put("0", "clun5@gmail.com"); + params.put("1", "clun5@gmail.com"); + + Insert stmt1 = QueryBuilder.insertInto("users") + .value("email", params.get("0")) + .value("age", 30); + Update.Where stmt4= QueryBuilder.update("users") + .with(QueryBuilder.set("age", 50)) + .where(QueryBuilder.eq("email", params.get("0"))); + Batch stmt5 = QueryBuilder.batch(stmt1).add(stmt4); + SESSION.execute(stmt5); + return params; + } + @Trace(dispatcher = true) + private Map batchStmt() { + Map params = new HashMap<>(); + params.put("0", "22"); + params.put("1", "test@gmail.com"); + BoundStatement b1 = new BoundStatement(SESSION.prepare(QUERIES.get(5))).bind().setInt(0, 22).setString("email",params.get("1")); + CassandraTestUtils.SimpleStatementWrapper wrapper = new CassandraTestUtils.SimpleStatementWrapper(b1); + BatchStatement batchStmt = new BatchStatement().add(new SimpleStatement(QUERIES.get(4))).add(wrapper); + SESSION.execute(batchStmt); + + return params; + } + @Trace(dispatcher = true) + private Map nestedBatchStmt() { + Map params = new HashMap<>(); + params.put("0", "22"); + params.put("1", "test@gmail.com"); + + BoundStatement b1 = new BoundStatement(SESSION.prepare(QUERIES.get(5))).bind().setInt(0, 22).setString("email",params.get("1")); + BatchStatement batchStmt = new BatchStatement().add(new SimpleStatement(QUERIES.get(4))).add(b1); + + BatchStatement batchStmt2 = new BatchStatement().add(batchStmt); + SESSION.execute(batchStmt2); + return params; + } + @Trace(dispatcher = true) + private Map batchStmtWithBuiltStmt() { + Map params = new HashMap<>(); + params.put("0", "clun5@gmail.com"); + + Insert stmt1 = QueryBuilder.insertInto("users") + .value("age", 35) + .value("email", params.get("0")); + Update.Where stmt4= QueryBuilder.update("users") + .with(QueryBuilder.set("age", 50)) + .where(QueryBuilder.eq("email", params.get("0"))); + + BatchStatement batchStmt2 = new BatchStatement().add(stmt4).add(stmt1); + SESSION.execute(batchStmt2); + return params; + } + + @Trace(dispatcher = true) + private Map customCodecCase() { + try (Cluster newCluster = Cluster.builder().withPort(PORT).addContactPoint("127.0.0.1").withCodecRegistry(new CodecRegistry()).build()) { + Session newSession = newCluster.connect("test"); + + CodecRegistry codecRegistry = newCluster.getConfiguration().getCodecRegistry(); + codecRegistry.register(new CassandraTestUtils.DateTimeCodec()); + + Map params = CassandraTestUtils.getBoundParams(); + + BoundStatement boundStmt = newSession.prepare(QUERIES.get(7)).bind(); + boundStmt.setUUID(0, UUID.fromString(params.get("0"))); + boundStmt.setBool("isMarried", Boolean.parseBoolean(params.get("3"))); + boundStmt.set("email", params.get("1"), TypeCodec.varchar()); + boundStmt.setString("email", params.get("1")); + boundStmt.setInt(2, Integer.parseInt(params.get("2"))); + boundStmt.set(2, Integer.parseInt(params.get("2")), TypeCodec.cint()); + boundStmt.setBytes("img", ByteBuffer.wrap("data".getBytes())); + boundStmt.setDecimal("phone", new BigDecimal(22222222)); + boundStmt.set("phone", new BigDecimal(22222222), BigDecimal.class); + boundStmt.setDate("dob", LocalDate.fromDaysSinceEpoch(100)); + boundStmt.setString("name", "ishi"); + boundStmt.setList("events", new ArrayList<>()); + boundStmt.setSet("address", new HashSet<>()); + boundStmt.setMap("marks", new HashMap<>()); + newSession.execute(boundStmt); + + newSession.closeAsync(); + return params; + } + } + + @Trace(dispatcher = true) + private Map customCodecCase1() { + try (Cluster newCluster = Cluster.builder().withPort(PORT).addContactPoint("127.0.0.1").withCodecRegistry(new CodecRegistry()).build()) { + CodecRegistry codecRegistry = newCluster.getConfiguration().getCodecRegistry(); + codecRegistry.register(new CassandraTestUtils.DateTimeCodec()); + + Session newSession = newCluster.connect("test"); + newSession.execute(QUERIES.get(8)); + + + Map params = CassandraTestUtils.getCustomCodecParams(); + + SimpleStatement stmt = new SimpleStatement(QUERIES.get(9), UUID.fromString(params.get("0")), DateTime.parse(params.get("1"))); + newSession.execute(stmt); + return params; + } + } +} diff --git a/instrumentation-security/cassandra-datastax-3/src/test/java/com/nr/agent/security/instrumentation/cassandra3/CassandraTestUtils.java b/instrumentation-security/cassandra-datastax-3/src/test/java/com/nr/agent/security/instrumentation/cassandra3/CassandraTestUtils.java new file mode 100644 index 000000000..326449fd5 --- /dev/null +++ b/instrumentation-security/cassandra-datastax-3/src/test/java/com/nr/agent/security/instrumentation/cassandra3/CassandraTestUtils.java @@ -0,0 +1,118 @@ +package com.nr.agent.security.instrumentation.cassandra3; + +import com.datastax.driver.core.DataType; +import com.datastax.driver.core.LocalDate; +import com.datastax.driver.core.ProtocolVersion; +import com.datastax.driver.core.Statement; +import com.datastax.driver.core.StatementWrapper; +import com.datastax.driver.core.TypeCodec; +import com.datastax.driver.core.exceptions.InvalidTypeException; +import org.joda.time.DateTime; + +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +class CassandraTestUtils { + private static final String KEYSPACE = "test"; + static Map getBoundParams () { + Map params = new HashMap<>(); + params.put("0", UUID.randomUUID().toString()); + params.put("1", "idawda@gmail.com"); + params.put("2", String.valueOf(22)); + params.put("3", String.valueOf(false)); + params.put("5", String.valueOf(new BigDecimal(22222222))); + params.put("6", String.valueOf(LocalDate.fromDaysSinceEpoch(100))); + params.put("7", "ishi"); + params.put("8", String.valueOf(new ArrayList<>())); + params.put("9", String.valueOf(new HashSet<>())); + params.put("10", String.valueOf(new HashMap<>())); + return params; + } + static Map getValueParams() { + Map params = new HashMap<>(); + params.put("0", "35"); + params.put("1", "bob1@example.com"); + return params; + } + + static Map getNamedParams() { + Map params = new HashMap<>(); + params.put("age", "35"); + params.put("email", "bob1@example.com"); + return params; + } + static Map getCustomCodecParams() { + Map params = new HashMap<>(); + params.put("0", String.valueOf(UUID.randomUUID())); + params.put("1", String.valueOf(DateTime.now())); + return params; + } + static List getQueries(){ + List QUERIES = new ArrayList<>(); + // SCHEMA BASED QUERIES + QUERIES.add("CREATE KEYSPACE " + KEYSPACE + " WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };"); + QUERIES.add("USE " + KEYSPACE + ";"); + QUERIES.add("CREATE TABLE users (email text, age int, PRIMARY KEY (email));"); + QUERIES.add("CREATE TABLE users2 (id uuid PRIMARY KEY, email text, age int, isMarried boolean, img blob, phone decimal, dob DATE, name varchar, events list, address set, marks map);"); + + // QUERIES FOR TABLE, index starts from 4 + QUERIES.add("INSERT INTO users (age, email) VALUES (35, 'bob@example.com');"); + QUERIES.add("INSERT INTO users (age, email) VALUES (?, ?);"); + QUERIES.add("INSERT INTO users (age, email) VALUES (:age, :email);"); + QUERIES.add("INSERT INTO users2 (id, email, age, isMarried, img, phone, dob, name, events, address, marks) VALUES (?,?,?,?,?,?,?,?,?,?,?);"); + + // QUERY for creating a table with custom codec DateTimeCodec + QUERIES.add("CREATE TABLE users3 (id uuid PRIMARY KEY, timestamp TIMESTAMP);"); + QUERIES.add("INSERT INTO users3 (id, timestamp) VALUES (?, ?);"); + QUERIES.add("INSERT INTO users (age,email) VALUES (35,?);"); + QUERIES.add("BEGIN BATCH INSERT INTO users (email,age) VALUES (?,30);UPDATE users SET age=50 WHERE email=?;APPLY BATCH;"); + QUERIES.add("SELECT * FROM users WHERE email=?;"); + QUERIES.add("DELETE FROM users WHERE email=?;"); + QUERIES.add("UPDATE users SET age=50 WHERE email=?;"); + QUERIES.add("SELECT * FROM users WHERE email='clun5@gmail.com';"); + QUERIES.add("INSERT INTO users (age,email) VALUES (35,'clun5@gmail.com');"); + QUERIES.add("DELETE FROM users WHERE email='clun5@gmail.com';"); + QUERIES.add("UPDATE users SET age=50 WHERE email='clun5@gmail.com';"); + return QUERIES; + } + + static class SimpleStatementWrapper extends StatementWrapper { + public SimpleStatementWrapper(Statement statement) { + super(statement); + } + } + static class DateTimeCodec extends TypeCodec { + public DateTimeCodec() { + super(DataType.timestamp(), DateTime.class); + } + @Override + public DateTime parse(String value) { + if (value == null || value.equals("NULL")) return null; + try { + return DateTime.parse(value); + } catch (IllegalArgumentException iae) { + throw new InvalidTypeException("Could not parse format: " + value, iae); + } + } + @Override + public String format(DateTime value) { + if (value == null) + return "NULL"; + return Long.toString(value.getMillis()); + } + @Override + public ByteBuffer serialize(DateTime value, ProtocolVersion protocolVersion) { + return value == null ? null : TypeCodec.bigint().serialize(value.getMillis(), protocolVersion); + } + @Override + public DateTime deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) { + return bytes == null || bytes.remaining() == 0 ? null: new DateTime(TypeCodec.bigint().deserializeNoBoxing(bytes, protocolVersion)); + } + } +} diff --git a/instrumentation-security/cassandra-datastax-3/src/test/resources/cu-cassandra.yaml b/instrumentation-security/cassandra-datastax-3/src/test/resources/cu-cassandra.yaml new file mode 100644 index 000000000..b307135d2 --- /dev/null +++ b/instrumentation-security/cassandra-datastax-3/src/test/resources/cu-cassandra.yaml @@ -0,0 +1,593 @@ +# Cassandra storage config YAML + +# NOTE: +# See http://wiki.apache.org/cassandra/StorageConfiguration for +# full explanations of configuration directives +# /NOTE + +# The name of the cluster. This is mainly used to prevent machines in +# one logical cluster from joining another. +cluster_name: 'Test Cluster' + +# You should always specify InitialToken when setting up a production +# cluster for the first time, and often when adding capacity later. +# The principle is that each node should be given an equal slice of +# the token ring; see http://wiki.apache.org/cassandra/Operations +# for more details. +# +# If blank, Cassandra will request a token bisecting the range of +# the heaviest-loaded existing node. If there is no load information +# available, such as is the case with a new cluster, it will pick +# a random token, which will lead to hot spots. +#initial_token: + +# See http://wiki.apache.org/cassandra/HintedHandoff +hinted_handoff_enabled: true +# this defines the maximum amount of time a dead host will have hints +# generated. After it has been dead this long, new hints for it will not be +# created until it has been seen alive and gone down again. +max_hint_window_in_ms: 10800000 # 3 hours +# Maximum throttle in KBs per second, per delivery thread. This will be +# reduced proportionally to the number of nodes in the cluster. (If there +# are two nodes in the cluster, each delivery thread will use the maximum +# rate; if there are three, each will throttle to half of the maximum, +# since we expect two nodes to be delivering hints simultaneously.) +hinted_handoff_throttle_in_kb: 1024 +# Number of threads with which to deliver hints; +# Consider increasing this number when you have multi-dc deployments, since +# cross-dc handoff tends to be slower +max_hints_delivery_threads: 2 + +# The following setting populates the page cache on memtable flush and compaction +# WARNING: Enable this setting only when the whole node's data fits in memory. +# Defaults to: false +# populate_io_cache_on_flush: false + +# Authentication backend, implementing IAuthenticator; used to identify users +# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthenticator, +# PasswordAuthenticator}. +# +# - AllowAllAuthenticator performs no checks - set it to disable authentication. +# - PasswordAuthenticator relies on username/password pairs to authenticate +# users. It keeps usernames and hashed passwords in system_auth.credentials table. +# Please increase system_auth keyspace replication factor if you use this authenticator. +authenticator: AllowAllAuthenticator + +# Authorization backend, implementing IAuthorizer; used to limit access/provide permissions +# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllAuthorizer, +# CassandraAuthorizer}. +# +# - AllowAllAuthorizer allows any action to any user - set it to disable authorization. +# - CassandraAuthorizer stores permissions in system_auth.permissions table. Please +# increase system_auth keyspace replication factor if you use this authorizer. +authorizer: AllowAllAuthorizer + +# Validity period for permissions cache (fetching permissions can be an +# expensive operation depending on the authorizer, CassandraAuthorizer is +# one example). Defaults to 2000, set to 0 to disable. +# Will be disabled automatically for AllowAllAuthorizer. +permissions_validity_in_ms: 2000 + + +# The partitioner is responsible for distributing rows (by key) across +# nodes in the cluster. Any IPartitioner may be used, including your +# own as long as it is on the classpath. Out of the box, Cassandra +# provides org.apache.cassandra.dht.{Murmur3Partitioner, RandomPartitioner +# ByteOrderedPartitioner, OrderPreservingPartitioner (deprecated)}. +# +# - RandomPartitioner distributes rows across the cluster evenly by md5. +# This is the default prior to 1.2 and is retained for compatibility. +# - Murmur3Partitioner is similar to RandomPartioner but uses Murmur3_128 +# Hash Function instead of md5. When in doubt, this is the best option. +# - ByteOrderedPartitioner orders rows lexically by key bytes. BOP allows +# scanning rows in key order, but the ordering can generate hot spots +# for sequential insertion workloads. +# - OrderPreservingPartitioner is an obsolete form of BOP, that stores +# - keys in a less-efficient format and only works with keys that are +# UTF8-encoded Strings. +# - CollatingOPP collates according to EN,US rules rather than lexical byte +# ordering. Use this as an example if you need custom collation. +# +# See http://wiki.apache.org/cassandra/Operations for more on +# partitioners and token selection. +partitioner: org.apache.cassandra.dht.Murmur3Partitioner + +# directories where Cassandra should store data on disk. +data_file_directories: + - target/embeddedCassandra/data + +hints_directory: target/embeddedCassandra/data/hints + +cdc_raw_directory: target/embeddedCassandra/data/cdc_raw + +# commit log +commitlog_directory: target/embeddedCassandra/commitlog + +# policy for data disk failures: +# stop: shut down gossip and Thrift, leaving the node effectively dead, but +# can still be inspected via JMX. +# best_effort: stop using the failed disk and respond to requests based on +# remaining available sstables. This means you WILL see obsolete +# data at CL.ONE! +# ignore: ignore fatal errors and let requests fail, as in pre-1.2 Cassandra +disk_failure_policy: stop + + +# Maximum size of the key cache in memory. +# +# Each key cache hit saves 1 seek and each row cache hit saves 2 seeks at the +# minimum, sometimes more. The key cache is fairly tiny for the amount of +# time it saves, so it's worthwhile to use it at large numbers. +# The row cache saves even more time, but must store the whole values of +# its rows, so it is extremely space-intensive. It's best to only use the +# row cache if you have hot rows or static rows. +# +# NOTE: if you reduce the size, you may not get you hottest keys loaded on startup. +# +# Default value is empty to make it "auto" (min(5% of Heap (in MB), 100MB)). Set to 0 to disable key cache. +key_cache_size_in_mb: + +# Duration in seconds after which Cassandra should +# safe the keys cache. Caches are saved to saved_caches_directory as +# specified in this configuration file. +# +# Saved caches greatly improve cold-start speeds, and is relatively cheap in +# terms of I/O for the key cache. Row cache saving is much more expensive and +# has limited use. +# +# Default is 14400 or 4 hours. +key_cache_save_period: 14400 + +# Number of keys from the key cache to save +# Disabled by default, meaning all keys are going to be saved +# key_cache_keys_to_save: 100 + +# Maximum size of the row cache in memory. +# NOTE: if you reduce the size, you may not get you hottest keys loaded on startup. +# +# Default value is 0, to disable row caching. +row_cache_size_in_mb: 0 + +# Duration in seconds after which Cassandra should +# safe the row cache. Caches are saved to saved_caches_directory as specified +# in this configuration file. +# +# Saved caches greatly improve cold-start speeds, and is relatively cheap in +# terms of I/O for the key cache. Row cache saving is much more expensive and +# has limited use. +# +# Default is 0 to disable saving the row cache. +row_cache_save_period: 0 + +# Number of keys from the row cache to save +# Disabled by default, meaning all keys are going to be saved +# row_cache_keys_to_save: 100 + +# saved caches +saved_caches_directory: target/embeddedCassandra/saved_caches + +# commitlog_sync may be either "periodic" or "batch." +# When in batch mode, Cassandra won't ack writes until the commit log +# has been fsynced to disk. It will wait up to +# commitlog_sync_batch_window_in_ms milliseconds for other writes, before +# performing the sync. +# +# commitlog_sync: batch +# commitlog_sync_batch_window_in_ms: 50 +# +# the other option is "periodic" where writes may be acked immediately +# and the CommitLog is simply synced every commitlog_sync_period_in_ms +# milliseconds. +commitlog_sync: periodic +commitlog_sync_period_in_ms: 10000 + +# The size of the individual commitlog file segments. A commitlog +# segment may be archived, deleted, or recycled once all the data +# in it (potentially from each columnfamily in the system) has been +# flushed to sstables. +# +# The default size is 32, which is almost always fine, but if you are +# archiving commitlog segments (see commitlog_archiving.properties), +# then you probably want a finer granularity of archiving; 8 or 16 MB +# is reasonable. +commitlog_segment_size_in_mb: 32 + +# any class that implements the SeedProvider interface and has a +# constructor that takes a Map of parameters will do. +seed_provider: + # Addresses of hosts that are deemed contact points. + # Cassandra nodes use this list of hosts to find each other and learn + # the topology of the ring. You must change this if you are running + # multiple nodes! + - class_name: org.apache.cassandra.locator.SimpleSeedProvider + parameters: + # seeds is actually a comma-delimited list of addresses. + # Ex: ",," + - seeds: "127.0.0.1" + + +# For workloads with more data than can fit in memory, Cassandra's +# bottleneck will be reads that need to fetch data from +# disk. "concurrent_reads" should be set to (16 * number_of_drives) in +# order to allow the operations to enqueue low enough in the stack +# that the OS and drives can reorder them. +# +# On the other hand, since writes are almost never IO bound, the ideal +# number of "concurrent_writes" is dependent on the number of cores in +# your system; (8 * number_of_cores) is a good rule of thumb. +concurrent_reads: 32 +concurrent_writes: 32 + +# Total memory to use for memtables. Cassandra will flush the largest +# memtable when this much memory is used. +# If omitted, Cassandra will set it to 1/3 of the heap. +# memtable_total_space_in_mb: 2048 + +# Total space to use for commitlogs. +# If space gets above this value (it will round up to the next nearest +# segment multiple), Cassandra will flush every dirty CF in the oldest +# segment and remove it. +# commitlog_total_space_in_mb: 4096 + +# This sets the amount of memtable flush writer threads. These will +# be blocked by disk io, and each one will hold a memtable in memory +# while blocked. If you have a large heap and many data directories, +# you can increase this value for better flush performance. +# By default this will be set to the amount of data directories defined. +#memtable_flush_writers: 1 + +# the number of full memtables to allow pending flush, that is, +# waiting for a writer thread. At a minimum, this should be set to +# the maximum number of secondary indexes created on a single CF. +#memtable_flush_queue_size: 4 + +# Whether to, when doing sequential writing, fsync() at intervals in +# order to force the operating system to flush the dirty +# buffers. Enable this to avoid sudden dirty buffer flushing from +# impacting read latencies. Almost always a good idea on SSD:s; not +# necessarily on platters. +trickle_fsync: false +trickle_fsync_interval_in_kb: 10240 + +# TCP port, for commands and data +# 0 - pick random port +storage_port: 0 + +# SSL port, for encrypted communication. Unused unless enabled in +# encryption_options +ssl_storage_port: 1018 + +# Address to bind to and tell other Cassandra nodes to connect to. You +# _must_ change this if you want multiple nodes to be able to +# communicate! +# +# Leaving it blank leaves it up to InetAddress.getLocalHost(). This +# will always do the Right Thing *if* the node is properly configured +# (hostname, name resolution, etc), and the Right Thing is to use the +# address associated with the hostname (it might not be). +# +# Setting this to 0.0.0.0 is always wrong. +listen_address: 127.0.0.1 + +start_native_transport: true +# port for the CQL native transport to listen for clients on +# 0 - pick random port +native_transport_port: 0 + +# Whether to start the thrift rpc server. +start_rpc: true + +# Address to broadcast to other Cassandra nodes +# Leaving this blank will set it to the same value as listen_address +# broadcast_address: 1.2.3.4 + +# The address to bind the Thrift RPC service to -- clients connect +# here. Unlike ListenAddress above, you *can* specify 0.0.0.0 here if +# you want Thrift to listen on all interfaces. +# +# Leaving this blank has the same effect it does for ListenAddress, +# (i.e. it will be based on the configured hostname of the node). +rpc_address: localhost +# port for Thrift to listen for clients on +# 0 - pick random port +rpc_port: 0 + +# enable or disable keepalive on rpc connections +rpc_keepalive: true + +# Cassandra provides three options for the RPC Server: +# +# sync -> One connection per thread in the rpc pool (see below). +# For a very large number of clients, memory will be your limiting +# factor; on a 64 bit JVM, 128KB is the minimum stack size per thread. +# Connection pooling is very, very strongly recommended. +# +# async -> Nonblocking server implementation with one thread to serve +# rpc connections. This is not recommended for high throughput use +# cases. Async has been tested to be about 50% slower than sync +# or hsha and is deprecated: it will be removed in the next major release. +# +# hsha -> Stands for "half synchronous, half asynchronous." The rpc thread pool +# (see below) is used to manage requests, but the threads are multiplexed +# across the different clients. +# +# The default is sync because on Windows hsha is about 30% slower. On Linux, +# sync/hsha performance is about the same, with hsha of course using less memory. +rpc_server_type: sync + +# Uncomment rpc_min|max|thread to set request pool size. +# You would primarily set max for the sync server to safeguard against +# misbehaved clients; if you do hit the max, Cassandra will block until one +# disconnects before accepting more. The defaults for sync are min of 16 and max +# unlimited. +# +# For the Hsha server, the min and max both default to quadruple the number of +# CPU cores. +# +# This configuration is ignored by the async server. +# +# rpc_min_threads: 16 +# rpc_max_threads: 2048 + +# uncomment to set socket buffer sizes on rpc connections +# rpc_send_buff_size_in_bytes: +# rpc_recv_buff_size_in_bytes: + +# Frame size for thrift (maximum field length). +# 0 disables TFramedTransport in favor of TSocket. This option +# is deprecated; we strongly recommend using Framed mode. +thrift_framed_transport_size_in_mb: 15 + +# The max length of a thrift message, including all fields and +# internal thrift overhead. +thrift_max_message_length_in_mb: 16 + +# Set to true to have Cassandra create a hard link to each sstable +# flushed or streamed locally in a backups/ subdirectory of the +# Keyspace data. Removing these links is the operator's +# responsibility. +incremental_backups: false + +# Whether or not to take a snapshot before each compaction. Be +# careful using this option, since Cassandra won't clean up the +# snapshots for you. Mostly useful if you're paranoid when there +# is a data format change. +snapshot_before_compaction: false + +# Whether or not a snapshot is taken of the data before keyspace truncation +# or dropping of column families. The STRONGLY advised default of true +# should be used to provide data safety. If you set this flag to false, you will +# lose data on truncation or drop. +auto_snapshot: false + +# Add column indexes to a row after its contents reach this size. +# Increase if your column values are large, or if you have a very large +# number of columns. The competing causes are, Cassandra has to +# deserialize this much of the row to read a single column, so you want +# it to be small - at least if you do many partial-row reads - but all +# the index data is read for each access, so you don't want to generate +# that wastefully either. +column_index_size_in_kb: 64 + +# Size limit for rows being compacted in memory. Larger rows will spill +# over to disk and use a slower two-pass compaction process. A message +# will be logged specifying the row key. +#in_memory_compaction_limit_in_mb: 64 + +# Number of simultaneous compactions to allow, NOT including +# validation "compactions" for anti-entropy repair. Simultaneous +# compactions can help preserve read performance in a mixed read/write +# workload, by mitigating the tendency of small sstables to accumulate +# during a single long running compactions. The default is usually +# fine and if you experience problems with compaction running too +# slowly or too fast, you should look at +# compaction_throughput_mb_per_sec first. +# +# This setting has no effect on LeveledCompactionStrategy. +# +# concurrent_compactors defaults to the number of cores. +# Uncomment to make compaction mono-threaded, the pre-0.8 default. +#concurrent_compactors: 1 + +# Multi-threaded compaction. When enabled, each compaction will use +# up to one thread per core, plus one thread per sstable being merged. +# This is usually only useful for SSD-based hardware: otherwise, +# your concern is usually to get compaction to do LESS i/o (see: +# compaction_throughput_mb_per_sec), not more. +#multithreaded_compaction: false + +# Throttles compaction to the given total throughput across the entire +# system. The faster you insert data, the faster you need to compact in +# order to keep the sstable count down, but in general, setting this to +# 16 to 32 times the rate you are inserting data is more than sufficient. +# Setting this to 0 disables throttling. Note that this account for all types +# of compaction, including validation compaction. +compaction_throughput_mb_per_sec: 16 + +# Track cached row keys during compaction, and re-cache their new +# positions in the compacted sstable. Disable if you use really large +# key caches. +#compaction_preheat_key_cache: true + +# Throttles all outbound streaming file transfers on this node to the +# given total throughput in Mbps. This is necessary because Cassandra does +# mostly sequential IO when streaming data during bootstrap or repair, which +# can lead to saturating the network connection and degrading rpc performance. +# When unset, the default is 200 Mbps or 25 MB/s. +# stream_throughput_outbound_megabits_per_sec: 200 + +# How long the coordinator should wait for read operations to complete +read_request_timeout_in_ms: 5000 +# How long the coordinator should wait for seq or index scans to complete +range_request_timeout_in_ms: 10000 +# How long the coordinator should wait for writes to complete +write_request_timeout_in_ms: 2000 +# How long a coordinator should continue to retry a CAS operation +# that contends with other proposals for the same row +cas_contention_timeout_in_ms: 1000 +# How long the coordinator should wait for truncates to complete +# (This can be much longer, because unless auto_snapshot is disabled +# we need to flush first so we can snapshot before removing the data.) +truncate_request_timeout_in_ms: 60000 +# The default timeout for other, miscellaneous operations +request_timeout_in_ms: 10000 + +# Enable operation timeout information exchange between nodes to accurately +# measure request timeouts. If disabled, replicas will assume that requests +# were forwarded to them instantly by the coordinator, which means that +# under overload conditions we will waste that much extra time processing +# already-timed-out requests. +# +# Warning: before enabling this property make sure to ntp is installed +# and the times are synchronized between the nodes. +cross_node_timeout: false + +# Enable socket timeout for streaming operation. +# When a timeout occurs during streaming, streaming is retried from the start +# of the current file. This _can_ involve re-streaming an important amount of +# data, so you should avoid setting the value too low. +# Default value is 0, which never timeout streams. +# streaming_socket_timeout_in_ms: 0 + +# phi value that must be reached for a host to be marked down. +# most users should never need to adjust this. +# phi_convict_threshold: 8 + +# endpoint_snitch -- Set this to a class that implements +# IEndpointSnitch. The snitch has two functions: +# - it teaches Cassandra enough about your network topology to route +# requests efficiently +# - it allows Cassandra to spread replicas around your cluster to avoid +# correlated failures. It does this by grouping machines into +# "datacenters" and "racks." Cassandra will do its best not to have +# more than one replica on the same "rack" (which may not actually +# be a physical location) +# +# IF YOU CHANGE THE SNITCH AFTER DATA IS INSERTED INTO THE CLUSTER, +# YOU MUST RUN A FULL REPAIR, SINCE THE SNITCH AFFECTS WHERE REPLICAS +# ARE PLACED. +# +# Out of the box, Cassandra provides +# - SimpleSnitch: +# Treats Strategy order as proximity. This improves cache locality +# when disabling read repair, which can further improve throughput. +# Only appropriate for single-datacenter deployments. +# - PropertyFileSnitch: +# Proximity is determined by rack and data center, which are +# explicitly configured in cassandra-topology.properties. +# - RackInferringSnitch: +# Proximity is determined by rack and data center, which are +# assumed to correspond to the 3rd and 2nd octet of each node's +# IP address, respectively. Unless this happens to match your +# deployment conventions (as it did Facebook's), this is best used +# as an example of writing a custom Snitch class. +# - Ec2Snitch: +# Appropriate for EC2 deployments in a single Region. Loads Region +# and Availability Zone information from the EC2 API. The Region is +# treated as the Datacenter, and the Availability Zone as the rack. +# Only private IPs are used, so this will not work across multiple +# Regions. +# - Ec2MultiRegionSnitch: +# Uses public IPs as broadcast_address to allow cross-region +# connectivity. (Thus, you should set seed addresses to the public +# IP as well.) You will need to open the storage_port or +# ssl_storage_port on the public IP firewall. (For intra-Region +# traffic, Cassandra will switch to the private IP after +# establishing a connection.) +# +# You can use a custom Snitch by setting this to the full class name +# of the snitch, which will be assumed to be on your classpath. +endpoint_snitch: SimpleSnitch + +# controls how often to perform the more expensive part of host score +# calculation +dynamic_snitch_update_interval_in_ms: 100 +# controls how often to reset all host scores, allowing a bad host to +# possibly recover +dynamic_snitch_reset_interval_in_ms: 600000 +# if set greater than zero and read_repair_chance is < 1.0, this will allow +# 'pinning' of replicas to hosts in order to increase cache capacity. +# The badness threshold will control how much worse the pinned host has to be +# before the dynamic snitch will prefer other replicas over it. This is +# expressed as a double which represents a percentage. Thus, a value of +# 0.2 means Cassandra would continue to prefer the static snitch values +# until the pinned host was 20% worse than the fastest. +dynamic_snitch_badness_threshold: 0.1 + +# request_scheduler -- Set this to a class that implements +# RequestScheduler, which will schedule incoming client requests +# according to the specific policy. This is useful for multi-tenancy +# with a single Cassandra cluster. +# NOTE: This is specifically for requests from the client and does +# not affect inter node communication. +# org.apache.cassandra.scheduler.NoScheduler - No scheduling takes place +# org.apache.cassandra.scheduler.RoundRobinScheduler - Round robin of +# client requests to a node with a separate queue for each +# request_scheduler_id. The scheduler is further customized by +# request_scheduler_options as described below. +request_scheduler: org.apache.cassandra.scheduler.NoScheduler + +# Scheduler Options vary based on the type of scheduler +# NoScheduler - Has no options +# RoundRobin +# - throttle_limit -- The throttle_limit is the number of in-flight +# requests per client. Requests beyond +# that limit are queued up until +# running requests can complete. +# The value of 80 here is twice the number of +# concurrent_reads + concurrent_writes. +# - default_weight -- default_weight is optional and allows for +# overriding the default which is 1. +# - weights -- Weights are optional and will default to 1 or the +# overridden default_weight. The weight translates into how +# many requests are handled during each turn of the +# RoundRobin, based on the scheduler id. +# +# request_scheduler_options: +# throttle_limit: 80 +# default_weight: 5 +# weights: +# Keyspace1: 1 +# Keyspace2: 5 + +# request_scheduler_id -- An identifer based on which to perform +# the request scheduling. Currently the only valid option is keyspace. +# request_scheduler_id: keyspace + +# index_interval controls the sampling of entries from the primrary +# row index in terms of space versus time. The larger the interval, +# the smaller and less effective the sampling will be. In technicial +# terms, the interval coresponds to the number of index entries that +# are skipped between taking each sample. All the sampled entries +# must fit in memory. Generally, a value between 128 and 512 here +# coupled with a large key cache size on CFs results in the best trade +# offs. This value is not often changed, however if you have many +# very small rows (many to an OS page), then increasing this will +# often lower memory usage without a impact on performance. +index_interval: 128 + +# Enable or disable inter-node encryption +# Default settings are TLS v1, RSA 1024-bit keys (it is imperative that +# users generate their own keys) TLS_RSA_WITH_AES_128_CBC_SHA as the cipher +# suite for authentication, key exchange and encryption of the actual data transfers. +# NOTE: No custom encryption options are enabled at the moment +# The available internode options are : all, none, dc, rack +# +# If set to dc cassandra will encrypt the traffic between the DCs +# If set to rack cassandra will encrypt the traffic between the racks +# +# The passwords used in these options must match the passwords used when generating +# the keystore and truststore. For instructions on generating these files, see: +# http://download.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html#CreateKeystore +# +encryption_options: + internode_encryption: none + keystore: conf/.keystore + keystore_password: cassandra + truststore: conf/.truststore + truststore_password: cassandra + # More advanced defaults below: + # protocol: TLS + # algorithm: SunX509 + # store_type: JKS + # cipher_suites: [TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA] \ No newline at end of file diff --git a/instrumentation-security/cassandra-datastax-3/src/test/resources/libs/libsigar-universal-macosx.dylib b/instrumentation-security/cassandra-datastax-3/src/test/resources/libs/libsigar-universal-macosx.dylib new file mode 100644 index 000000000..27ab10711 Binary files /dev/null and b/instrumentation-security/cassandra-datastax-3/src/test/resources/libs/libsigar-universal-macosx.dylib differ diff --git a/instrumentation-security/cassandra-datastax-3/src/test/resources/libs/libsigar-universal64-macosx.dylib b/instrumentation-security/cassandra-datastax-3/src/test/resources/libs/libsigar-universal64-macosx.dylib new file mode 100644 index 000000000..0c721fecf Binary files /dev/null and b/instrumentation-security/cassandra-datastax-3/src/test/resources/libs/libsigar-universal64-macosx.dylib differ diff --git a/instrumentation-security/cassandra-datastax-3/src/test/resources/libs/sigar.jar b/instrumentation-security/cassandra-datastax-3/src/test/resources/libs/sigar.jar new file mode 100644 index 000000000..58c733c6a Binary files /dev/null and b/instrumentation-security/cassandra-datastax-3/src/test/resources/libs/sigar.jar differ diff --git a/instrumentation-security/cassandra-datastax-3/src/test/resources/log4j-embedded-cassandra.properties b/instrumentation-security/cassandra-datastax-3/src/test/resources/log4j-embedded-cassandra.properties new file mode 100644 index 000000000..b2195ad80 --- /dev/null +++ b/instrumentation-security/cassandra-datastax-3/src/test/resources/log4j-embedded-cassandra.properties @@ -0,0 +1,33 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# for production, you should probably set the root to INFO +# and the pattern to %c instead of %l. (%l is slower.) + +# output messages into a rolling log file as well as stdout +log4j.rootLogger=ERROR,stdout,HColumnFamilyLogger + +# stdout +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d [%t] %-5p %c{3} - %m%n +log4j.appender.stdout.follow=true + +log4j.appender.HColumnFamilyLogger=org.apache.log4j.ConsoleAppender +log4j.appender.HColumnFamilyLogger.layout=org.apache.log4j.PatternLayout +log4j.appender.HColumnFamilyLogger.layout.ConversionPattern=%m%n +log4j.category.HColumnFamilyLogger=DEBUG +#log4j.category.org.apache=INFO, stdout \ No newline at end of file diff --git a/instrumentation-security/cassandra-datastax-4/build.gradle b/instrumentation-security/cassandra-datastax-4/build.gradle new file mode 100644 index 000000000..21ce0743e --- /dev/null +++ b/instrumentation-security/cassandra-datastax-4/build.gradle @@ -0,0 +1,27 @@ +dependencies { + implementation(project(":newrelic-security-api")) + implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}") + implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}") + implementation("com.datastax.oss:java-driver-core:4.13.0") { transitive = false } + + testImplementation("org.cassandraunit:cassandra-unit:4.3.1.0") + testImplementation("com.datastax.oss:java-driver-query-builder:4.13.0") +} + +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.cassandra-datastax-4' } +} + +verifyInstrumentation { + passesOnly 'com.datastax.oss:java-driver-core:[4.0.0,)' + excludeRegex ".*(rc|beta|alpha).*" +} + +site { + title 'Cassandra' + type 'Datastore' +} + +test { + jvmArgs(["-Djava.library.path=src/test/resources/libs/"]) +} \ No newline at end of file diff --git a/instrumentation-security/cassandra-datastax-4/src/main/java/com/datastax/driver/core/Session_Instrumentation.java b/instrumentation-security/cassandra-datastax-4/src/main/java/com/datastax/driver/core/Session_Instrumentation.java new file mode 100644 index 000000000..d56c2242b --- /dev/null +++ b/instrumentation-security/cassandra-datastax-4/src/main/java/com/datastax/driver/core/Session_Instrumentation.java @@ -0,0 +1,36 @@ +package com.datastax.driver.core; + +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import com.newrelic.agent.security.instrumentation.cassandra4.CassandraUtils; + +@Weave(type = MatchType.Interface, originalName = "com.datastax.oss.driver.api.core.session.Session") +public class Session_Instrumentation { + public ResultT execute(RequestT request, GenericType resultType) { + AbstractOperation cqlOperation = null; + boolean isLockAcquired = CassandraUtils.acquireLockIfPossible(request.hashCode()); + + ResultT result = null; + try { + result = Weaver.callOriginal(); + if(isLockAcquired){ + cqlOperation = CassandraUtils.preProcessSecurityHook(this.getClass().getName(), request); + if(cqlOperation != null){ + NewRelicSecurity.getAgent().registerOperation(cqlOperation); + } + } + }catch (Exception ignored) { + } finally { + if(isLockAcquired){ + CassandraUtils.releaseLock(request.hashCode()); + } + } + CassandraUtils.registerExitOperation(isLockAcquired, cqlOperation); + return result; + } +} diff --git a/instrumentation-security/cassandra-datastax-4/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultPrepareRequest_Instrumentation.java b/instrumentation-security/cassandra-datastax-4/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultPrepareRequest_Instrumentation.java new file mode 100644 index 000000000..2eb00b360 --- /dev/null +++ b/instrumentation-security/cassandra-datastax-4/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultPrepareRequest_Instrumentation.java @@ -0,0 +1,16 @@ +package com.datastax.oss.driver.internal.core.cql; + +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.agent.security.instrumentation.cassandra4.CassandraUtils; + +@Weave(type = MatchType.ExactClass, originalName = "com.datastax.oss.driver.internal.core.cql.DefaultPrepareRequest") +public abstract class DefaultPrepareRequest_Instrumentation { + + public DefaultPrepareRequest_Instrumentation(SimpleStatement statement){ + NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute( + CassandraUtils.NR_SEC_CUSTOM_ATTRIB_CQL_STMT + hashCode(), CassandraUtils.setParams(statement)); + } +} diff --git a/instrumentation-security/cassandra-datastax-4/src/main/java/com/newrelic/agent/security/instrumentation/cassandra4/CassandraUtils.java b/instrumentation-security/cassandra-datastax-4/src/main/java/com/newrelic/agent/security/instrumentation/cassandra4/CassandraUtils.java new file mode 100644 index 000000000..8829e5b5b --- /dev/null +++ b/instrumentation-security/cassandra-datastax-4/src/main/java/com/newrelic/agent/security/instrumentation/cassandra4/CassandraUtils.java @@ -0,0 +1,138 @@ +package com.newrelic.agent.security.instrumentation.cassandra4; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.cql.BatchStatement; +import com.datastax.oss.driver.api.core.cql.BatchableStatement; +import com.datastax.oss.driver.api.core.cql.BoundStatement; +import com.datastax.oss.driver.api.core.cql.ColumnDefinition; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.PrepareRequest; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; +import com.newrelic.api.agent.security.schema.operation.BatchSQLOperation; +import com.newrelic.api.agent.security.schema.operation.SQLOperation; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class CassandraUtils { + public static final String METHOD_EXECUTE = "execute"; + public static final String NR_SEC_CUSTOM_ATTRIB_CQL_STMT = "NR-CQL-STMT"; + public static final String EVENT_CATEGORY = "CQL"; + public static final String NR_SEC_CASSANDRA_LOCK = "CASSANDRA_OPERATION_LOCK"; + public static boolean acquireLockIfPossible(int hashCode) { + try { + return GenericHelper.acquireLockIfPossible(NR_SEC_CASSANDRA_LOCK, hashCode); + } catch (Exception ignored){ + } + return false; + } + + public static AbstractOperation preProcessSecurityHook(String klass, RequestT request) { + try { + SQLOperation cqlOperation = new SQLOperation(klass, METHOD_EXECUTE); + cqlOperation.setCaseType(VulnerabilityCaseType.NOSQL_DB_COMMAND); + cqlOperation.setDbName(EVENT_CATEGORY); + + if (request instanceof BatchStatement){ + BatchSQLOperation batchOperation = new BatchSQLOperation(klass, METHOD_EXECUTE); + batchOperation.setCaseType(VulnerabilityCaseType.NOSQL_DB_COMMAND); + BatchStatement batchStmt = (BatchStatement) request; + + for (BatchableStatement batchableStatement : batchStmt) { + AbstractOperation operation = preProcessSecurityHook(klass, batchableStatement); + if (operation instanceof SQLOperation) + batchOperation.addOperation((SQLOperation) operation); + } + return batchOperation; + } + else if (request instanceof SimpleStatement) { + cqlOperation.setQuery(((SimpleStatement) request).getQuery()); + cqlOperation.setParams(setParams((SimpleStatement) request)); + return cqlOperation; + } + else if (request instanceof PrepareRequest) { + cqlOperation.setQuery(((PrepareRequest) request).getQuery()); + Map params = NewRelicSecurity.getAgent().getSecurityMetaData().getCustomAttribute( + NR_SEC_CUSTOM_ATTRIB_CQL_STMT+ request.hashCode(), Map.class); + if(params!=null){ + cqlOperation.setParams(params); + } + return cqlOperation; + } + else if (request instanceof BoundStatement) { + cqlOperation.setQuery(((BoundStatement) request).getPreparedStatement().getQuery()); + cqlOperation.setParams(setParams((BoundStatement) request)); + return cqlOperation; + } + } catch (Exception ignored) { + } + return null; + } + + public static Map setParams(BoundStatement statement) { + Map params = new HashMap<>(); + ColumnDefinitions variables = statement.getPreparedStatement().getVariableDefinitions(); + try{ + for (int i = 0; i < variables.size(); i++) { + ColumnDefinition variable = variables.get(i); + CodecRegistry codecRegistry = statement.codecRegistry(); + TypeCodec codec = codecRegistry.codecFor(variable.getType()); + Object value = statement.get(variable.getName(), codec); + if (!(value instanceof ByteBuffer)) { + params.put(String.valueOf(i), String.valueOf(value)); + } + } + } catch (Exception ignored){ + } + return params; + } + + public static Map setParams(SimpleStatement statement) { + Map params = new HashMap<>(); + + try{ + List values = statement.getPositionalValues(); + for (int i = 0; i < values.size(); i++) { + if (!(values.get(i) instanceof ByteBuffer)) { + params.put(String.valueOf(i), String.valueOf(values.get(i))); + } + } + + Map namedValues = statement.getNamedValues(); + for (Map.Entry namedVal : namedValues.entrySet()) { + if (!(namedVal.getValue() instanceof ByteBuffer)) { + params.put(namedVal.getKey().asInternal(), String.valueOf(namedVal.getValue())); + } + } + } catch (Exception ignored){ + } + return params; + } + + public static void releaseLock(int hashCode) { + try { + GenericHelper.releaseLock(NR_SEC_CASSANDRA_LOCK, hashCode); + } catch (Throwable ignored) { + } + } + + public static void registerExitOperation(boolean isLockAcquired, AbstractOperation operation) { + try { + if(operation == null || !isLockAcquired || !NewRelicSecurity.isHookProcessingActive() + || GenericHelper.skipExistsEvent()) { + return; + } + NewRelicSecurity.getAgent().registerExitEvent(operation); + } catch (Exception ignored) { + } + } +} diff --git a/instrumentation-security/cassandra-datastax-4/src/test/java/com/nr/agent/security/instrumentation/cassandra4/CassandraTest.java b/instrumentation-security/cassandra-datastax-4/src/test/java/com/nr/agent/security/instrumentation/cassandra4/CassandraTest.java new file mode 100644 index 000000000..f0a0f3ab6 --- /dev/null +++ b/instrumentation-security/cassandra-datastax-4/src/test/java/com/nr/agent/security/instrumentation/cassandra4/CassandraTest.java @@ -0,0 +1,576 @@ +package com.nr.agent.security.instrumentation.cassandra4; + +import com.datastax.oss.driver.api.core.cql.BatchStatement; +import com.datastax.oss.driver.api.core.cql.BatchType; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.querybuilder.QueryBuilder; +import com.newrelic.agent.security.introspec.InstrumentationTestConfig; +import com.newrelic.agent.security.introspec.SecurityInstrumentationTestRunner; +import com.newrelic.agent.security.introspec.SecurityIntrospector; +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; +import com.newrelic.api.agent.security.schema.operation.BatchSQLOperation; +import com.newrelic.api.agent.security.schema.operation.SQLOperation; +import com.newrelic.security.test.marker.Java11IncompatibleTest; +import com.newrelic.security.test.marker.Java17IncompatibleTest; +import com.newrelic.security.test.marker.Java9IncompatibleTest; +import com.newrelic.agent.security.instrumentation.cassandra4.CassandraUtils; +import org.apache.cassandra.io.util.FileUtils; +import org.cassandraunit.CassandraCQLUnit; +import org.cassandraunit.dataset.cql.ClassPathCQLDataSet; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; + +import java.io.File; +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +// Issue when running cassandra unit on Java 9+ - https://github.com/jsevellec/cassandra-unit/issues/249 +@RunWith(SecurityInstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = { "com.datastax", "com.newrelic.agent.security.instrumentation" }) +@Category({ Java9IncompatibleTest.class, Java11IncompatibleTest.class, Java17IncompatibleTest.class }) +public class CassandraTest { + @ClassRule + public static CassandraCQLUnit CASSANDRA = new CassandraCQLUnit(new ClassPathCQLDataSet("users.cql", "test")); + private static final List QUERIES = CassandraTestUtils.getQueries(); + @AfterClass + public static void after() { + FileUtils.deleteRecursive(new File("target/")); + } + @Test + public void testStringSimpleStmt() { + stringSimpleStmt(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(0), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + } + @Test + public void testSimpleStmtPositionalValues() { + simpleStmtPositionalValues(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(1), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + } + @Test + public void testSimpleStmtNamedValues() { + Map params = simpleStmtNamedValues(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(2), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + + @Test + public void testQueryBuilder() { + queryBuilder(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(6), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + } + + @Test + public void testBoundStmt() { + Map params = boundStmt(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(3), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + @Test + public void testBoundStmt1() { + Map params = boundStmt1(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(2), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + @Test + public void testBoundStmt2() { + Map params = boundStmt2(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(11), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + @Test + public void testQueryBuilderPositionalValues() { + Map params = queryBuilderPositionalValues(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(4), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + + @Test + public void testQueryBuilderNamedValues() { + Map params = queryBuilderNamedValues(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(5), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + @Test + public void testQueryBuilderInsertPositionalParams() { + Map params = queryBuilderInsertPositionalParams(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(1), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + @Test + public void testQueryBuilderInsertNamedParams() { + Map params = queryBuilderInsertNamedParams(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(2), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + @Test + public void testQueryBuilderUpdatePositionalParams() { + Map params = queryBuilderUpdatePositionalParams(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(9), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + @Test + public void testQqueryBuilderUpdateNamedParams() { + Map params = queryBuilderUpdateNamedParams(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(10), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + @Test + public void testQueryBuilderDeletePositionalParams() { + Map params = queryBuilderDeletePositionalParams(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(7), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + @Test + public void testQueryBuilderDeleteNamedParams() { + Map params = queryBuilderDeleteNamedParams(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(8), operation.getQuery()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + Assert.assertEquals("Wrong params detected", params, operation.getParams()); + } + @Test + public void testBatchStmt() { + Map params = batchStmt(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + BatchSQLOperation batchOperation = (BatchSQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, batchOperation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", batchOperation.getMethodName()); + Assert.assertEquals("Wrong number of operations detected", 3, batchOperation.getOperations().size()); + + Assert.assertEquals("Invalid Query detected.", QUERIES.get(7), batchOperation.getOperations().get(0).getQuery()); + Assert.assertEquals("Invalid Query detected.", QUERIES.get(1), batchOperation.getOperations().get(1).getQuery()); + Assert.assertEquals("Invalid Query detected.", QUERIES.get(9), batchOperation.getOperations().get(2).getQuery()); + + Assert.assertEquals("Wrong params detected", CassandraTestUtils.getValueParams(), batchOperation.getOperations().get(0).getParams()); + Assert.assertEquals("Wrong params detected", params, batchOperation.getOperations().get(2).getParams()); + params = CassandraTestUtils.getValueParams(); + params.put("1","35"); + Assert.assertEquals("Wrong params detected", params, batchOperation.getOperations().get(1).getParams()); + + for (SQLOperation operation: batchOperation.getOperations()) { + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + } + } + @Test + public void testNestedBatchStmt() { + Map params = nestedBatchStmt(); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + BatchSQLOperation batchOperation = (BatchSQLOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, batchOperation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", batchOperation.getMethodName()); + + Assert.assertEquals("Wrong number of operations detected", 3, batchOperation.getOperations().size()); + Assert.assertEquals("Invalid Query detected.", QUERIES.get(7), batchOperation.getOperations().get(0).getQuery()); + Assert.assertEquals("Invalid Query detected.", QUERIES.get(1), batchOperation.getOperations().get(1).getQuery()); + Assert.assertEquals("Invalid Query detected.", QUERIES.get(9), batchOperation.getOperations().get(2).getQuery()); + + Assert.assertEquals("Wrong params detected", CassandraTestUtils.getValueParams(), batchOperation.getOperations().get(0).getParams()); + Assert.assertEquals("Wrong params detected", params, batchOperation.getOperations().get(2).getParams()); + params = CassandraTestUtils.getValueParams(); + params.put("1","35"); + Assert.assertEquals("Wrong params detected", params, batchOperation.getOperations().get(1).getParams()); + + for (SQLOperation operation: batchOperation.getOperations()) { + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.NOSQL_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + Assert.assertEquals("Invalid DB-Name.", CassandraUtils.EVENT_CATEGORY, operation.getDbName()); + } + } + + @Trace(dispatcher = true) + private void stringSimpleStmt() { + CASSANDRA.getSession().execute(QUERIES.get(0)); + } + @Trace(dispatcher = true) + private void simpleStmtPositionalValues() { + ArrayList positionalValue = new ArrayList<>(); + positionalValue.add("test@gmail.com"); + positionalValue.add(2); + CASSANDRA.getSession().execute(SimpleStatement.newInstance(QUERIES.get(1)).setPositionalValues(positionalValue)); + } + @Trace(dispatcher = true) + private Map simpleStmtNamedValues() { + Map namedValues = CassandraTestUtils.getNamedParams(); + namedValues.put("age", "35"); + + Map params = new HashMap<>(); + params.put("age", 35); + params.put("email", "bob1@example.com"); + + CASSANDRA.getSession().execute(SimpleStatement.newInstance(QUERIES.get(2)).setNamedValues(params)); + return namedValues; + } + @Trace(dispatcher = true) + private void queryBuilder() { + CASSANDRA.getSession().execute(QueryBuilder.selectFrom("users").all().build()); + } + + @Trace(dispatcher = true) + private Map queryBuilderPositionalValues() { + ArrayList positionalValue = new ArrayList<>(); + positionalValue.add("bob1@example.com"); + CASSANDRA.getSession().executeAsync( + QueryBuilder.selectFrom("users").all() + .whereColumn("email") + .isEqualTo(QueryBuilder.bindMarker()) + .build() + .setPositionalValues(positionalValue) + ); + return CassandraTestUtils.getValueParams(); + } + @Trace(dispatcher = true) + private Map queryBuilderNamedValues() { + Map params = new HashMap<>(); + params.put("email", "bob1@example.com"); + CASSANDRA.getSession().execute( + QueryBuilder.selectFrom("users").all() + .whereColumn("email") + .isEqualTo(QueryBuilder.bindMarker("email")) + .build() + .setNamedValues(params) + ); + return CassandraTestUtils.getNamedParams(); + } + @Trace(dispatcher = true) + private Map boundStmt() { + Map params = CassandraTestUtils.getBoundParams(); + + CASSANDRA.getSession().prepare(SimpleStatement.builder(QUERIES.get(3)) + .addPositionalValue(UUID.fromString(params.get("0"))) + .addPositionalValue(params.get("1")) + .addPositionalValue(Integer.parseInt(params.get("2"))) + .addPositionalValue(Boolean.parseBoolean(params.get("3"))) + .addPositionalValue(ByteBuffer.wrap("data".getBytes())) + .addPositionalValue(new BigDecimal(22222222)) + .addPositionalValue(LocalDate.of(2000,1,1)) + .addPositionalValue("ishi") + .addPositionalValue(new ArrayList<>()) + .addPositionalValue(new HashSet<>()) + .addPositionalValue(new HashMap<>()) + .build()).bind(); + return params; + } + + @Trace(dispatcher = true) + private Map boundStmt1() { + Map namedValues = CassandraTestUtils.getNamedParams(); + namedValues.put("age", "35"); + + Map params = new HashMap<>(); + params.put("age", 35); + params.put("email", "bob1@example.com"); + CASSANDRA.getSession().prepare(SimpleStatement.newInstance(QUERIES.get(2)).setNamedValues(params)).bind(); + return namedValues; + } + + @Trace(dispatcher = true) + private Map boundStmt2() { + CASSANDRA.getSession().prepare(QUERIES.get(11)).bind(); + return new HashMap<>(); + } + @Trace(dispatcher = true) + private Map queryBuilderInsertPositionalParams() { + ArrayList obj = new ArrayList<>(); + obj.add("bob1@example.com"); + obj.add(35); + + Map positionalValues = new HashMap<>(); + positionalValues.put("0", "bob1@example.com"); + positionalValues.put("1", "35"); + + SimpleStatement query = QueryBuilder + .insertInto("users") + .value("email", QueryBuilder.bindMarker()) + .value("age", QueryBuilder.bindMarker()) + .build().setPositionalValues(obj); + CASSANDRA.session.execute(query); + return positionalValues; + } + @Trace(dispatcher = true) + private Map queryBuilderInsertNamedParams() { + Map obj = new HashMap<>(); + obj.put("email", "bob1@example.com"); + obj.put("age", 35); + + Map namedValues = CassandraTestUtils.getNamedParams(); + namedValues.put("age", "35"); + + SimpleStatement query = QueryBuilder + .insertInto("users") + .value("email", QueryBuilder.bindMarker("email")) + .value("age", QueryBuilder.bindMarker("age")) + .build().setNamedValues(obj); + CASSANDRA.session.executeAsync(query); + return namedValues; + } + @Trace(dispatcher = true) + private Map queryBuilderUpdatePositionalParams() { + ArrayList obj = new ArrayList<>(); + obj.add(35); + obj.add("bob1@example.com"); + + Map params = new HashMap<>(); + params.put("0", "35"); + params.put("1", "bob1@example.com"); + + SimpleStatement query = QueryBuilder + .update("users") + .setColumn("age", QueryBuilder.bindMarker()) + .whereColumn("email").isEqualTo(QueryBuilder.bindMarker()) + .build().setPositionalValues(obj); + + CASSANDRA.session.executeAsync(query); + return params; + } + @Trace(dispatcher = true) + private Map queryBuilderUpdateNamedParams() { + Map obj = new HashMap<>(); + obj.put("email", "bob1@example.com"); + obj.put("age", 35); + + Map namedValues = CassandraTestUtils.getNamedParams(); + namedValues.put("age", "35"); + + SimpleStatement query = QueryBuilder + .update("users") + .setColumn("age", QueryBuilder.bindMarker("age")) + .whereColumn("email").isEqualTo(QueryBuilder.bindMarker("email")) + .build().setNamedValues(obj); + CASSANDRA.session.execute(query); + return namedValues; + } + @Trace(dispatcher = true) + private Map queryBuilderDeletePositionalParams() { + Map params = new HashMap<>(); + params.put("0", "test@gmail.com"); + + ArrayList obj = new ArrayList<>(); + obj.add("test@gmail.com"); + + SimpleStatement query = QueryBuilder + .deleteFrom("users") + .whereColumn("email").isEqualTo(QueryBuilder.bindMarker()) + .build().setPositionalValues(obj); + CASSANDRA.session.execute(query); + return params; + } + @Trace(dispatcher = true) + private Map queryBuilderDeleteNamedParams() { + Map params = new HashMap<>(); + params.put("email", "test@gmail.com"); + + Map obj = new HashMap<>(); + obj.put("email", "test@gmail.com"); + + SimpleStatement query = QueryBuilder + .deleteFrom("users") + .whereColumn("email").isEqualTo(QueryBuilder.bindMarker("email")) + .build().setNamedValues(obj); + CASSANDRA.session.executeAsync(query); + return params; + } + @Trace(dispatcher = true) + private Map batchStmt() { + Map params = new HashMap<>(); + params.put("0", "35"); + params.put("1", "bob1@example.com"); + + ArrayList obj = new ArrayList<>(); + obj.add("bob1@example.com"); + obj.add(35); + + ArrayList obj1 = new ArrayList<>(); + obj1.add(35); + obj1.add("bob1@example.com"); + + BatchStatement batchStmt = BatchStatement.builder(BatchType.UNLOGGED) + .addStatement(SimpleStatement.builder(QUERIES.get(7)).addPositionalValue("bob1@example.com").build()) + .addStatement(SimpleStatement.builder(QUERIES.get(1)).addPositionalValues(obj).build()) + .addStatement(SimpleStatement.builder(QUERIES.get(9)).addPositionalValues(obj1).build()).build(); + + CASSANDRA.getSession().execute(batchStmt); + return params; + } + @Trace(dispatcher = true) + private Map nestedBatchStmt() { + Map params = new HashMap<>(); + params.put("1", "bob1@example.com"); + params.put("0", "35"); + + ArrayList obj = new ArrayList<>(); + obj.add("bob1@example.com"); + obj.add(35); + + ArrayList obj1 = new ArrayList<>(); + obj1.add(35); + obj1.add("bob1@example.com"); + + BatchStatement batchStmt = BatchStatement.builder(BatchType.UNLOGGED) + .addStatement(SimpleStatement.builder(QUERIES.get(7)).addPositionalValue("bob1@example.com").build()) + .addStatement(SimpleStatement.builder(QUERIES.get(1)).addPositionalValues(obj).build()) + .addStatement(SimpleStatement.builder(QUERIES.get(9)).addPositionalValues(obj1).build()).build(); + + BatchStatement batchStmt1 = BatchStatement.builder(BatchType.UNLOGGED).addStatements(batchStmt).build(); + CASSANDRA.getSession().execute(batchStmt1); + return params; + } +} diff --git a/instrumentation-security/cassandra-datastax-4/src/test/java/com/nr/agent/security/instrumentation/cassandra4/CassandraTestUtils.java b/instrumentation-security/cassandra-datastax-4/src/test/java/com/nr/agent/security/instrumentation/cassandra4/CassandraTestUtils.java new file mode 100644 index 000000000..0acf99e37 --- /dev/null +++ b/instrumentation-security/cassandra-datastax-4/src/test/java/com/nr/agent/security/instrumentation/cassandra4/CassandraTestUtils.java @@ -0,0 +1,59 @@ +package com.nr.agent.security.instrumentation.cassandra4; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +class CassandraTestUtils { + static Map getBoundParams () { + Map params = new HashMap<>(); + params.put("0", UUID.randomUUID().toString()); + params.put("1", "idawda@gmail.com"); + params.put("2", String.valueOf(22)); + params.put("3", String.valueOf(false)); + params.put("5", String.valueOf(new BigDecimal(22222222))); + params.put("6", String.valueOf(LocalDate.of(2000,1,1))); + params.put("7", "ishi"); + params.put("8", String.valueOf(new ArrayList<>())); + params.put("9", String.valueOf(new HashSet<>())); + params.put("10", String.valueOf(new HashMap<>())); + return params; + } + static Map getValueParams() { + Map params = new HashMap<>(); + params.put("0", "bob1@example.com"); + return params; + } + + static Map getNamedParams() { + Map params = new HashMap<>(); + params.put("email", "bob1@example.com"); + return params; + } + + static List getQueries() { + List QUERIES = new ArrayList<>(); + + QUERIES.add("INSERT INTO users (age, email) VALUES (35, 'bob@example.com')"); + QUERIES.add("INSERT INTO users (email,age) VALUES (?,?)"); + QUERIES.add("INSERT INTO users (email,age) VALUES (:email,:age)"); + QUERIES.add("INSERT INTO users2 (id, email, age, isMarried, img, phone, dob, name, events, address, marks) VALUES (?,?,?,?,?,?,?,?,?,?,?)"); + + QUERIES.add("SELECT * FROM users WHERE email=?"); + QUERIES.add("SELECT * FROM users WHERE email=:email"); + QUERIES.add("SELECT * FROM users"); + + QUERIES.add("DELETE FROM users WHERE email=?"); + QUERIES.add("DELETE FROM users WHERE email=:email"); + + QUERIES.add("UPDATE users SET age=? WHERE email=?"); + QUERIES.add("UPDATE users SET age=:age WHERE email=:email"); + QUERIES.add("SELECT * FROM users where email='bob@example.com'"); + return QUERIES; + } +} diff --git a/instrumentation-security/cassandra-datastax-4/src/test/resources/libs/libsigar-universal-macosx.dylib b/instrumentation-security/cassandra-datastax-4/src/test/resources/libs/libsigar-universal-macosx.dylib new file mode 100644 index 000000000..27ab10711 Binary files /dev/null and b/instrumentation-security/cassandra-datastax-4/src/test/resources/libs/libsigar-universal-macosx.dylib differ diff --git a/instrumentation-security/cassandra-datastax-4/src/test/resources/libs/libsigar-universal64-macosx.dylib b/instrumentation-security/cassandra-datastax-4/src/test/resources/libs/libsigar-universal64-macosx.dylib new file mode 100644 index 000000000..0c721fecf Binary files /dev/null and b/instrumentation-security/cassandra-datastax-4/src/test/resources/libs/libsigar-universal64-macosx.dylib differ diff --git a/instrumentation-security/cassandra-datastax-4/src/test/resources/libs/sigar.jar b/instrumentation-security/cassandra-datastax-4/src/test/resources/libs/sigar.jar new file mode 100644 index 000000000..58c733c6a Binary files /dev/null and b/instrumentation-security/cassandra-datastax-4/src/test/resources/libs/sigar.jar differ diff --git a/instrumentation-security/cassandra-datastax-4/src/test/resources/users.cql b/instrumentation-security/cassandra-datastax-4/src/test/resources/users.cql new file mode 100644 index 000000000..69c21c496 --- /dev/null +++ b/instrumentation-security/cassandra-datastax-4/src/test/resources/users.cql @@ -0,0 +1,14 @@ +CREATE TABLE users (email text, age int, PRIMARY KEY (email)); +CREATE TABLE users2 ( + id uuid PRIMARY KEY, + email text, + age int, + isMarried boolean, + img blob, + phone decimal, + dob DATE, + name varchar, + events list, + address set, + marks map +); diff --git a/instrumentation-security/dynamodb-2.1.2/src/main/java/com/newrelic/agent/security/instrumentation/dynamodb_212/DynamoDBUtil.java b/instrumentation-security/dynamodb-2.1.2/src/main/java/com/newrelic/agent/security/instrumentation/dynamodb_212/DynamoDBUtil.java index 35600235d..cf361ced0 100644 --- a/instrumentation-security/dynamodb-2.1.2/src/main/java/com/newrelic/agent/security/instrumentation/dynamodb_212/DynamoDBUtil.java +++ b/instrumentation-security/dynamodb-2.1.2/src/main/java/com/newrelic/agent/security/instrumentation/dynamodb_212/DynamoDBUtil.java @@ -286,7 +286,7 @@ else if (value instanceof TransactWriteItemsRequest) { query.setTableName(conditionCheck.tableName()); query.setConditionExpression(conditionCheck.conditionExpression()); query.setExpressionAttributeNames(conditionCheck.expressionAttributeNames()); - query.setExpressionAttributeNames(conditionCheck.expressionAttributeNames()); + query.setExpressionAttributeValues(conditionCheck.expressionAttributeValues()); requests.add(new DynamoDBRequest(query, OP_READ)); } if (transactItems.get(i).put() != null) { diff --git a/instrumentation-security/dynamodb-2.1.2/src/main/java/software/amazon/awssdk/core/client/handler/AsyncClientHandler_Instrumentation.java b/instrumentation-security/dynamodb-2.1.2/src/main/java/software/amazon/awssdk/core/client/handler/AsyncClientHandler_Instrumentation.java index 3110471c3..aa67bdfcb 100644 --- a/instrumentation-security/dynamodb-2.1.2/src/main/java/software/amazon/awssdk/core/client/handler/AsyncClientHandler_Instrumentation.java +++ b/instrumentation-security/dynamodb-2.1.2/src/main/java/software/amazon/awssdk/core/client/handler/AsyncClientHandler_Instrumentation.java @@ -7,6 +7,7 @@ package software.amazon.awssdk.core.client.handler; +import com.newrelic.agent.security.instrumentation.dynamodb_212.DynamoDBUtil; import com.newrelic.api.agent.security.schema.AbstractOperation; import com.newrelic.api.agent.weaver.MatchType; import com.newrelic.api.agent.weaver.Weave; diff --git a/instrumentation-security/dynamodb-2.1.2/src/main/java/software/amazon/awssdk/core/client/handler/SyncClientHandler_Instrumentation.java b/instrumentation-security/dynamodb-2.1.2/src/main/java/software/amazon/awssdk/core/client/handler/SyncClientHandler_Instrumentation.java index 074d4e364..4429b7d3a 100644 --- a/instrumentation-security/dynamodb-2.1.2/src/main/java/software/amazon/awssdk/core/client/handler/SyncClientHandler_Instrumentation.java +++ b/instrumentation-security/dynamodb-2.1.2/src/main/java/software/amazon/awssdk/core/client/handler/SyncClientHandler_Instrumentation.java @@ -7,6 +7,7 @@ package software.amazon.awssdk.core.client.handler; +import com.newrelic.agent.security.instrumentation.dynamodb_212.DynamoDBUtil; import com.newrelic.api.agent.security.schema.AbstractOperation; import com.newrelic.api.agent.weaver.MatchType; import com.newrelic.api.agent.weaver.Weave; diff --git a/instrumentation-security/dynamodb-2.1.2/src/test/java/com/nr/agent/security/instrumentation/dynamodb_212/DynamodbTest.java b/instrumentation-security/dynamodb-2.1.2/src/test/java/com/nr/agent/security/instrumentation/dynamodb_212/DynamodbTest.java index ee7eb25f5..f7fafce30 100644 --- a/instrumentation-security/dynamodb-2.1.2/src/test/java/com/nr/agent/security/instrumentation/dynamodb_212/DynamodbTest.java +++ b/instrumentation-security/dynamodb-2.1.2/src/test/java/com/nr/agent/security/instrumentation/dynamodb_212/DynamodbTest.java @@ -24,6 +24,7 @@ import software.amazon.awssdk.services.dynamodb.model.AttributeValueUpdate; import software.amazon.awssdk.services.dynamodb.model.BatchGetItemRequest; import software.amazon.awssdk.services.dynamodb.model.BatchWriteItemRequest; +import software.amazon.awssdk.services.dynamodb.model.ConditionCheck; import software.amazon.awssdk.services.dynamodb.model.Delete; import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest; import software.amazon.awssdk.services.dynamodb.model.DescribeTableRequest; @@ -237,6 +238,88 @@ else if (i==1) { } } + @Test + public void testTransactWriteItemsAsync() { + transactWriteItemsAsync(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + DynamoDBOperation operation = (DynamoDBOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.DYNAMO_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "transactWriteItems", operation.getMethodName()); + Assert.assertEquals("Invalid operation Category.", DynamoDBOperation.Category.DQL, operation.getCategory()); + Assert.assertTrue("No payload detected", operation.getPayload().size() > 0); + + int i = 0; + for(DynamoDBRequest request: operation.getPayload()) { + if (i==0) { + Map query = (Map) request.getQuery().getKey(); + Assert.assertEquals("Invalid table name", "test", request.getQuery().getTableName()); + Assert.assertNotNull("No such payload detected", query.get("artist")); + Assert.assertEquals("Invalid payload value.", "Monu",query.get("artist").s()); + Assert.assertEquals("Invalid query-type.", "delete", request.getQueryType()); + } + else if (i==1) { + DynamoDBRequest.Query query = request.getQuery(); + Map data = (Map) query.getKey(); + Assert.assertEquals("Invalid table name", "test", request.getQuery().getTableName()); + Assert.assertNotNull("No such payload detected", data.get("year")); + Assert.assertEquals("Invalid payload value.", "1998", data.get("year").n()); + Assert.assertEquals("Invalid condition expression.", "#y = :a", query.getConditionExpression()); + Assert.assertNotNull("No expression attribute name.", query.getExpressionAttributeNames()); + Assert.assertEquals("Invalid expression attribute name.", "artist", ((Map)query.getExpressionAttributeNames()).get("#y")); + Assert.assertNotNull("No expression attribute value.", query.getExpressionAttributeValues()); + Assert.assertEquals("Invalid expression attribute value.", "Monu", ((Map)query.getExpressionAttributeValues()).get(":a").s()); + Assert.assertEquals("Invalid query-type.", "read", request.getQueryType()); + } + else if (i==2) { + Map query = (Map) request.getQuery().getItem(); + Assert.assertEquals("Invalid table name", "test", request.getQuery().getTableName()); + Assert.assertNotNull("No such payload detected", query.get("artist")); + Assert.assertEquals("Invalid payload value.", "Red",query.get("artist").s()); + Assert.assertNotNull("No such payload detected", query.get("year")); + Assert.assertEquals("Invalid payload value.", "1998",query.get("year").n()); + Assert.assertEquals("Invalid query-type.", "write", request.getQueryType()); + } + i++; + } + } + + @Test + public void testTransactWriteItemsConditionCheck() { + transactWriteItemsConditionCheck(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + DynamoDBOperation operation = (DynamoDBOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.DYNAMO_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "transactWriteItems", operation.getMethodName()); + Assert.assertEquals("Invalid operation Category.", DynamoDBOperation.Category.DQL, operation.getCategory()); + Assert.assertTrue("No payload detected", operation.getPayload().size() > 0); + + int i = 0; + for(DynamoDBRequest request: operation.getPayload()) { + if (i==0) { + DynamoDBRequest.Query query = request.getQuery(); + Map data = (Map) query.getKey(); + Assert.assertEquals("Invalid table name", "test", request.getQuery().getTableName()); + Assert.assertNotNull("No such payload detected", data.get("artist")); + Assert.assertEquals("Invalid payload value.", "Red", data.get("artist").s()); + Assert.assertEquals("Invalid condition expression.", "#y = :a", query.getConditionExpression()); + Assert.assertNotNull("No expression attribute name.", query.getExpressionAttributeNames()); + Assert.assertEquals("Invalid expression attribute name.", "year", ((Map)query.getExpressionAttributeNames()).get("#y")); + Assert.assertNotNull("No expression attribute value.", query.getExpressionAttributeValues()); + Assert.assertEquals("Invalid expression attribute value.", "1998", ((Map)query.getExpressionAttributeValues()).get(":a").n()); + Assert.assertEquals("Invalid query-type.", "read", request.getQueryType()); + } + i++; + } + } + @Test public void testDeleteItem() { deleteItemTxn(); @@ -796,4 +879,39 @@ public void transactWriteItems() { client.transactWriteItems(queryRequest); } + + public void transactWriteItemsAsync() { + Map key1 = new HashMap<>(); + key1.put("artist", AttributeValue.builder().s("Monu").build()); + Map key2 = new HashMap<>(); + key2.put("year", AttributeValue.builder().n("1998").build()); + Map key3 = new HashMap<>(); + key3.put("artist", AttributeValue.builder().s("Red").build()); + key3.put("year", AttributeValue.builder().n("1998").build()); + + TransactWriteItemsRequest queryRequest = TransactWriteItemsRequest.builder().transactItems( + TransactWriteItem.builder().delete(Delete.builder().tableName(DynamoUtil.TABLE).key(key1).build()).build(), + TransactWriteItem.builder().conditionCheck(ConditionCheck.builder().tableName(DynamoUtil.TABLE).key(key2) + .conditionExpression("#y = :a") + .expressionAttributeNames(Collections.singletonMap("#y","artist")) + .expressionAttributeValues(Collections.singletonMap(":a", AttributeValue.builder().s("Monu").build())) + .build()).build(), + TransactWriteItem.builder().put(Put.builder().tableName(DynamoUtil.TABLE).item(key3).build()).build()).build(); + + asyncClient.transactWriteItems(queryRequest); + } + + public void transactWriteItemsConditionCheck() { + Map key2 = new HashMap<>(); + key2.put("artist", AttributeValue.builder().s("Red").build()); + + TransactWriteItemsRequest queryRequest = TransactWriteItemsRequest.builder().transactItems( + TransactWriteItem.builder().conditionCheck(ConditionCheck.builder().tableName(DynamoUtil.TABLE).key(key2) + .conditionExpression("#y = :a") + .expressionAttributeNames(Collections.singletonMap("#y","year")) + .expressionAttributeValues(Collections.singletonMap(":a", AttributeValue.builder().n("1998").build())) + .build()).build()).build(); + + client.transactWriteItems(queryRequest); + } } diff --git a/instrumentation-security/dynamodb-2.15.34/src/main/java/com/newrelic/agent/security/instrumentation/dynamodb_215/DynamoDBUtil.java b/instrumentation-security/dynamodb-2.15.34/src/main/java/com/newrelic/agent/security/instrumentation/dynamodb_215/DynamoDBUtil.java index f676de8d6..da5ae8560 100644 --- a/instrumentation-security/dynamodb-2.15.34/src/main/java/com/newrelic/agent/security/instrumentation/dynamodb_215/DynamoDBUtil.java +++ b/instrumentation-security/dynamodb-2.15.34/src/main/java/com/newrelic/agent/security/instrumentation/dynamodb_215/DynamoDBUtil.java @@ -291,7 +291,7 @@ else if (value instanceof TransactWriteItemsRequest) { query.setTableName(conditionCheck.tableName()); query.setConditionExpression(conditionCheck.conditionExpression()); query.setExpressionAttributeNames(conditionCheck.expressionAttributeNames()); - query.setExpressionAttributeNames(conditionCheck.expressionAttributeNames()); + query.setExpressionAttributeValues(conditionCheck.expressionAttributeValues()); requests.add(new DynamoDBRequest(query, OP_READ)); } if (transactItems.get(i).put() != null) { diff --git a/instrumentation-security/dynamodb-2.15.34/src/main/java/software/amazon/awssdk/core/client/handler/AsyncClientHandler_Instrumentation.java b/instrumentation-security/dynamodb-2.15.34/src/main/java/software/amazon/awssdk/core/client/handler/AsyncClientHandler_Instrumentation.java index 6d6843f89..eb3ce587d 100644 --- a/instrumentation-security/dynamodb-2.15.34/src/main/java/software/amazon/awssdk/core/client/handler/AsyncClientHandler_Instrumentation.java +++ b/instrumentation-security/dynamodb-2.15.34/src/main/java/software/amazon/awssdk/core/client/handler/AsyncClientHandler_Instrumentation.java @@ -7,6 +7,7 @@ package software.amazon.awssdk.core.client.handler; +import com.newrelic.agent.security.instrumentation.dynamodb_215.DynamoDBUtil; import com.newrelic.api.agent.security.schema.AbstractOperation; import com.newrelic.api.agent.weaver.MatchType; import com.newrelic.api.agent.weaver.Weave; diff --git a/instrumentation-security/dynamodb-2.15.34/src/main/java/software/amazon/awssdk/core/client/handler/SyncClientHandler_Instrumentation.java b/instrumentation-security/dynamodb-2.15.34/src/main/java/software/amazon/awssdk/core/client/handler/SyncClientHandler_Instrumentation.java index 24f812967..a110863ce 100644 --- a/instrumentation-security/dynamodb-2.15.34/src/main/java/software/amazon/awssdk/core/client/handler/SyncClientHandler_Instrumentation.java +++ b/instrumentation-security/dynamodb-2.15.34/src/main/java/software/amazon/awssdk/core/client/handler/SyncClientHandler_Instrumentation.java @@ -7,6 +7,7 @@ package software.amazon.awssdk.core.client.handler; +import com.newrelic.agent.security.instrumentation.dynamodb_215.DynamoDBUtil; import com.newrelic.api.agent.security.schema.AbstractOperation; import com.newrelic.api.agent.weaver.MatchType; import com.newrelic.api.agent.weaver.Weave; diff --git a/instrumentation-security/dynamodb-2.15.34/src/test/java/com/nr/agent/security/instrumentation/dynamodb_215/DynamodbTest.java b/instrumentation-security/dynamodb-2.15.34/src/test/java/com/nr/agent/security/instrumentation/dynamodb_215/DynamodbTest.java index 3c58a81b1..f931d0b77 100644 --- a/instrumentation-security/dynamodb-2.15.34/src/test/java/com/nr/agent/security/instrumentation/dynamodb_215/DynamodbTest.java +++ b/instrumentation-security/dynamodb-2.15.34/src/test/java/com/nr/agent/security/instrumentation/dynamodb_215/DynamodbTest.java @@ -26,6 +26,7 @@ import software.amazon.awssdk.services.dynamodb.model.BatchGetItemRequest; import software.amazon.awssdk.services.dynamodb.model.BatchStatementRequest; import software.amazon.awssdk.services.dynamodb.model.BatchWriteItemRequest; +import software.amazon.awssdk.services.dynamodb.model.ConditionCheck; import software.amazon.awssdk.services.dynamodb.model.Delete; import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest; import software.amazon.awssdk.services.dynamodb.model.DescribeTableRequest; @@ -239,6 +240,39 @@ else if (i==1) { } } + @Test + public void testTransactWriteItemsConditionCheck() { + transactWriteItemsConditionCheck(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + DynamoDBOperation operation = (DynamoDBOperation) operations.get(0); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.DYNAMO_DB_COMMAND, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "transactWriteItems", operation.getMethodName()); + Assert.assertEquals("Invalid operation Category.", DynamoDBOperation.Category.DQL, operation.getCategory()); + Assert.assertTrue("No payload detected", operation.getPayload().size() > 0); + + int i = 0; + for(DynamoDBRequest request: operation.getPayload()) { + if (i==0) { + DynamoDBRequest.Query query = request.getQuery(); + Map data = (Map) query.getKey(); + Assert.assertEquals("Invalid table name", "test", request.getQuery().getTableName()); + Assert.assertNotNull("No such payload detected", data.get("artist")); + Assert.assertEquals("Invalid payload value.", "Red", data.get("artist").s()); + Assert.assertEquals("Invalid condition expression.", "#y = :a", query.getConditionExpression()); + Assert.assertNotNull("No expression attribute name.", query.getExpressionAttributeNames()); + Assert.assertEquals("Invalid expression attribute name.", "year", ((Map)query.getExpressionAttributeNames()).get("#y")); + Assert.assertNotNull("No expression attribute value.", query.getExpressionAttributeValues()); + Assert.assertEquals("Invalid expression attribute value.", "1998", ((Map)query.getExpressionAttributeValues()).get(":a").n()); + Assert.assertEquals("Invalid query-type.", "read", request.getQueryType()); + } + i++; + } + } + @Test public void testExecuteStmt() { executeStmtTxn(); @@ -389,6 +423,19 @@ public void testTransactWriteItemsAsync() { Assert.assertEquals("Invalid query-type.", "delete", request.getQueryType()); } else if (i==1) { + DynamoDBRequest.Query query = request.getQuery(); + Map data = (Map) query.getKey(); + Assert.assertEquals("Invalid table name", "test", request.getQuery().getTableName()); + Assert.assertNotNull("No such payload detected", data.get("year")); + Assert.assertEquals("Invalid payload value.", "1998", data.get("year").n()); + Assert.assertEquals("Invalid condition expression.", "#y = :a", query.getConditionExpression()); + Assert.assertNotNull("No expression attribute name.", query.getExpressionAttributeNames()); + Assert.assertEquals("Invalid expression attribute name.", "artist", ((Map)query.getExpressionAttributeNames()).get("#y")); + Assert.assertNotNull("No expression attribute value.", query.getExpressionAttributeValues()); + Assert.assertEquals("Invalid expression attribute value.", "Monu", ((Map)query.getExpressionAttributeValues()).get(":a").s()); + Assert.assertEquals("Invalid query-type.", "read", request.getQueryType()); + } + else if (i==2) { Map query = (Map) request.getQuery().getItem(); Assert.assertEquals("Invalid table name", "test", request.getQuery().getTableName()); Assert.assertNotNull("No such payload detected", query.get("artist")); @@ -1122,15 +1169,22 @@ public void transactGetItemsAsync() { } public void transactWriteItemsAsync() { - Map key = new HashMap<>(); - key.put("artist", AttributeValue.builder().s("Monu").build()); + Map key1 = new HashMap<>(); + key1.put("artist", AttributeValue.builder().s("Monu").build()); Map key2 = new HashMap<>(); - key2.put("artist", AttributeValue.builder().s("Red").build()); key2.put("year", AttributeValue.builder().n("1998").build()); + Map key3 = new HashMap<>(); + key3.put("artist", AttributeValue.builder().s("Red").build()); + key3.put("year", AttributeValue.builder().n("1998").build()); TransactWriteItemsRequest queryRequest = TransactWriteItemsRequest.builder().transactItems( - TransactWriteItem.builder().delete(Delete.builder().tableName(DynamoUtil.TABLE).key(key).build()).build(), - TransactWriteItem.builder().put(Put.builder().tableName(DynamoUtil.TABLE).item(key2).build()).build()).build(); + TransactWriteItem.builder().delete(Delete.builder().tableName(DynamoUtil.TABLE).key(key1).build()).build(), + TransactWriteItem.builder().conditionCheck(ConditionCheck.builder().tableName(DynamoUtil.TABLE).key(key2) + .conditionExpression("#y = :a") + .expressionAttributeNames(Collections.singletonMap("#y","artist")) + .expressionAttributeValues(Collections.singletonMap(":a", AttributeValue.builder().s("Monu").build())) + .build()).build(), + TransactWriteItem.builder().put(Put.builder().tableName(DynamoUtil.TABLE).item(key3).build()).build()).build(); asyncClient.transactWriteItems(queryRequest); } @@ -1184,4 +1238,18 @@ public void executeTransactionPrepAsync() .build(); asyncClient.executeTransaction(executeTransactionRequest); } + + public void transactWriteItemsConditionCheck() { + Map key2 = new HashMap<>(); + key2.put("artist", AttributeValue.builder().s("Red").build()); + + TransactWriteItemsRequest queryRequest = TransactWriteItemsRequest.builder().transactItems( + TransactWriteItem.builder().conditionCheck(ConditionCheck.builder().tableName(DynamoUtil.TABLE).key(key2) + .conditionExpression("#y = :a") + .expressionAttributeNames(Collections.singletonMap("#y","year")) + .expressionAttributeValues(Collections.singletonMap(":a", AttributeValue.builder().n("1998").build())) + .build()).build()).build(); + + client.transactWriteItems(queryRequest); + } } diff --git a/instrumentation-security/http-async-client-4/src/test/java/com/nr/agent/security/instrumentation/httpasyncclient4/HttpAsyncClient4Test.java b/instrumentation-security/http-async-client-4/src/test/java/com/nr/agent/security/instrumentation/httpasyncclient4/HttpAsyncClient4Test.java index e51d301da..acfc4dc4f 100644 --- a/instrumentation-security/http-async-client-4/src/test/java/com/nr/agent/security/instrumentation/httpasyncclient4/HttpAsyncClient4Test.java +++ b/instrumentation-security/http-async-client-4/src/test/java/com/nr/agent/security/instrumentation/httpasyncclient4/HttpAsyncClient4Test.java @@ -5,6 +5,8 @@ import com.newrelic.agent.security.introspec.SecurityIntrospector; import com.newrelic.agent.security.introspec.internal.HttpServerRule; import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; import com.newrelic.api.agent.security.schema.AbstractOperation; import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; import com.newrelic.api.agent.security.schema.operation.SSRFOperation; @@ -25,6 +27,8 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.List; +import java.util.Map; +import java.util.UUID; import java.util.concurrent.Future; @RunWith(SecurityInstrumentationTestRunner.class) @@ -44,76 +48,128 @@ public static void before() { } @Test public void testExecute() throws Exception { - callExecute(); + String headerValue = String.valueOf(UUID.randomUUID()); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + + callExecute(); List operations = introspector.getOperations(); Assert.assertTrue("No operations detected", operations.size() > 0); SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); Assert.assertEquals("Invalid executed parameters.", endpoint.toString(), operation.getArg()); Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + verifyHeaders(headerValue, headers); } @Test public void testExecute1() throws Exception { - callExecute1(); + String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + + callExecute1(); List operations = introspector.getOperations(); Assert.assertTrue("No operations detected", operations.size() > 0); SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); Assert.assertEquals("Invalid executed parameters.", endpoint.toString(), operation.getArg()); Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + verifyHeaders(headerValue, headers); } @Test public void testExecute2() throws Exception { - callExecute2(); + String headerValue = String.valueOf(UUID.randomUUID()); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + + callExecute2(); List operations = introspector.getOperations(); Assert.assertTrue("No operations detected", operations.size() > 0); SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); Assert.assertEquals("Invalid executed parameters.", endpoint.toString(), operation.getArg()); Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + verifyHeaders(headerValue, headers); } @Test public void testExecute3() throws Exception { - callExecute3(); + String headerValue = String.valueOf(UUID.randomUUID()); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + + callExecute3(); List operations = introspector.getOperations(); Assert.assertTrue("No operations detected", operations.size() > 0); SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); Assert.assertEquals("Invalid executed parameters.", endpoint.toString(), operation.getArg()); Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + verifyHeaders(headerValue, headers); } @Test public void testExecute4() throws Exception { - callExecute4(); + String headerValue = String.valueOf(UUID.randomUUID()); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + + callExecute4(); List operations = introspector.getOperations(); Assert.assertTrue("No operations detected", operations.size() > 0); SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); Assert.assertEquals("Invalid executed parameters.", endpoint.toString(), operation.getArg()); Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + verifyHeaders(headerValue, headers); } @Test public void testExecute5() throws Exception { - callExecute5(); + String headerValue = String.valueOf(UUID.randomUUID()); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + + callExecute5(); List operations = introspector.getOperations(); Assert.assertTrue("No operations detected", operations.size() > 0); SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); Assert.assertEquals("Invalid executed parameters.", endpoint.toString(), operation.getArg()); Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + verifyHeaders(headerValue, headers); + } + + private void setCSECHeaders(String headerValue, SecurityIntrospector introspector) { + introspector.setK2FuzzRequestId(headerValue+"a"); + introspector.setK2ParentId(headerValue+"b"); + introspector.setK2TracingData(headerValue); + } + + private void verifyHeaders(String headerValue, Map headers) { + Assert.assertTrue(String.format("Missing K2 header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue+"a", headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); + Assert.assertTrue(String.format("Missing K2 header: %s", GenericHelper.CSEC_PARENT_ID), headers.containsKey(GenericHelper.CSEC_PARENT_ID)); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", GenericHelper.CSEC_PARENT_ID), headerValue+"b", headers.get(GenericHelper.CSEC_PARENT_ID)); + Assert.assertTrue(String.format("Missing K2 header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", + headerValue), headers.get( + ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); } @Trace(dispatcher = true) diff --git a/instrumentation-security/httpclient-3/src/test/java/com/nr/agent/security/instrumentation/httpclient3/HttpClientTest.java b/instrumentation-security/httpclient-3/src/test/java/com/nr/agent/security/instrumentation/httpclient3/HttpClientTest.java index b52d203dd..2820bbe95 100644 --- a/instrumentation-security/httpclient-3/src/test/java/com/nr/agent/security/instrumentation/httpclient3/HttpClientTest.java +++ b/instrumentation-security/httpclient-3/src/test/java/com/nr/agent/security/instrumentation/httpclient3/HttpClientTest.java @@ -6,6 +6,7 @@ import com.newrelic.agent.security.introspec.SecurityIntrospector; import com.newrelic.agent.security.introspec.internal.HttpServerRule; import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; import com.newrelic.api.agent.security.schema.AbstractOperation; import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; @@ -35,8 +36,7 @@ public void testExecute() throws URISyntaxException, IOException { String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); - introspector.setK2FuzzRequestId(headerValue); - introspector.setK2TracingData(headerValue); + setCSECHeaders(headerValue, introspector); callExecute(); @@ -48,11 +48,7 @@ public void testExecute() throws URISyntaxException, IOException { Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue, headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", headerValue), headers.get( - ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + verifyHeaders(headerValue, headers); } @Test @@ -60,8 +56,7 @@ public void testExecute1() throws URISyntaxException, IOException { String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); - introspector.setK2FuzzRequestId(headerValue); - introspector.setK2TracingData(headerValue); + setCSECHeaders(headerValue, introspector); callExecute(); @@ -73,11 +68,7 @@ public void testExecute1() throws URISyntaxException, IOException { Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue, headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", headerValue), headers.get( - ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + verifyHeaders(headerValue, headers); } @Test @@ -85,8 +76,7 @@ public void testExecute2() throws URISyntaxException, IOException { String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); - introspector.setK2FuzzRequestId(headerValue); - introspector.setK2TracingData(headerValue); + setCSECHeaders(headerValue, introspector); callExecute(); @@ -98,10 +88,23 @@ public void testExecute2() throws URISyntaxException, IOException { Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue, headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", headerValue), headers.get( + verifyHeaders(headerValue, headers); + } + + private void setCSECHeaders(String headerValue, SecurityIntrospector introspector) { + introspector.setK2FuzzRequestId(headerValue+"a"); + introspector.setK2ParentId(headerValue+"b"); + introspector.setK2TracingData(headerValue); + } + + private void verifyHeaders(String headerValue, Map headers) { + Assert.assertTrue(String.format("Missing K2 header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue+"a", headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); + Assert.assertTrue(String.format("Missing K2 header: %s", GenericHelper.CSEC_PARENT_ID), headers.containsKey(GenericHelper.CSEC_PARENT_ID)); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", GenericHelper.CSEC_PARENT_ID), headerValue+"b", headers.get(GenericHelper.CSEC_PARENT_ID)); + Assert.assertTrue(String.format("Missing K2 header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", + headerValue), headers.get( ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); } diff --git a/instrumentation-security/httpclient-4.0/src/test/java/com/nr/agent/security/instrumentation/httpclient40/HttpClientTest.java b/instrumentation-security/httpclient-4.0/src/test/java/com/nr/agent/security/instrumentation/httpclient40/HttpClientTest.java index 116d224ce..79f723cf0 100644 --- a/instrumentation-security/httpclient-4.0/src/test/java/com/nr/agent/security/instrumentation/httpclient40/HttpClientTest.java +++ b/instrumentation-security/httpclient-4.0/src/test/java/com/nr/agent/security/instrumentation/httpclient40/HttpClientTest.java @@ -5,6 +5,7 @@ import com.newrelic.agent.security.introspec.SecurityIntrospector; import com.newrelic.agent.security.introspec.internal.HttpServerRule; import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; import com.newrelic.api.agent.security.schema.AbstractOperation; import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; @@ -39,8 +40,7 @@ public void testExecute() throws URISyntaxException, IOException { String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); - introspector.setK2FuzzRequestId(headerValue); - introspector.setK2TracingData(headerValue); + setCSECHeaders(headerValue, introspector); callExecute(); @@ -51,11 +51,7 @@ public void testExecute() throws URISyntaxException, IOException { Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue, headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", headerValue), headers.get( - ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + verifyHeaders(headerValue, headers); } @Test @@ -64,8 +60,7 @@ public void testExecute1() throws URISyntaxException, IOException { String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); - introspector.setK2FuzzRequestId(headerValue); - introspector.setK2TracingData(headerValue); + setCSECHeaders(headerValue, introspector); callExecute1(); List operations = introspector.getOperations(); @@ -75,11 +70,7 @@ public void testExecute1() throws URISyntaxException, IOException { Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue, headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", headerValue), headers.get( - ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + verifyHeaders(headerValue, headers); } @Test @@ -88,8 +79,7 @@ public void testExecute2() throws URISyntaxException, IOException { String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); - introspector.setK2FuzzRequestId(headerValue); - introspector.setK2TracingData(headerValue); + setCSECHeaders(headerValue, introspector); callExecute2(); List operations = introspector.getOperations(); @@ -99,11 +89,7 @@ public void testExecute2() throws URISyntaxException, IOException { Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue, headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", headerValue), headers.get( - ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + verifyHeaders(headerValue, headers); } @@ -113,8 +99,7 @@ public void testExecute3() throws URISyntaxException, IOException { String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); - introspector.setK2FuzzRequestId(headerValue); - introspector.setK2TracingData(headerValue); + setCSECHeaders(headerValue, introspector); callExecute3(); List operations = introspector.getOperations(); @@ -124,11 +109,7 @@ public void testExecute3() throws URISyntaxException, IOException { Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue, headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", headerValue), headers.get( - ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + verifyHeaders(headerValue, headers); } @@ -139,8 +120,7 @@ public void testExecute4() throws Exception { String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); - introspector.setK2FuzzRequestId(headerValue); - introspector.setK2TracingData(headerValue); + setCSECHeaders(headerValue, introspector); callExecute4(); List operations = introspector.getOperations(); @@ -150,11 +130,7 @@ public void testExecute4() throws Exception { Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue, headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", headerValue), headers.get( - ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + verifyHeaders(headerValue, headers); } @@ -165,8 +141,7 @@ public void testExecute5() throws Exception { String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); - introspector.setK2FuzzRequestId(headerValue); - introspector.setK2TracingData(headerValue); + setCSECHeaders(headerValue, introspector); callExecute5(); List operations = introspector.getOperations(); @@ -176,11 +151,7 @@ public void testExecute5() throws Exception { Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue, headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", headerValue), headers.get( - ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + verifyHeaders(headerValue, headers); } @@ -191,8 +162,7 @@ public void testExecute6() throws Exception { String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); - introspector.setK2FuzzRequestId(headerValue); - introspector.setK2TracingData(headerValue); + setCSECHeaders(headerValue, introspector); callExecute6(); List operations = introspector.getOperations(); @@ -202,11 +172,7 @@ public void testExecute6() throws Exception { Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue, headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", headerValue), headers.get( - ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + verifyHeaders(headerValue, headers); } @@ -217,8 +183,7 @@ public void testExecute7() throws Exception { String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); - introspector.setK2FuzzRequestId(headerValue); - introspector.setK2TracingData(headerValue); + setCSECHeaders(headerValue, introspector); callExecute7(); List operations = introspector.getOperations(); @@ -228,14 +193,27 @@ public void testExecute7() throws Exception { Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue, headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", headerValue), headers.get( - ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + verifyHeaders(headerValue, headers); } + private void setCSECHeaders(String headerValue, SecurityIntrospector introspector) { + introspector.setK2FuzzRequestId(headerValue+"a"); + introspector.setK2ParentId(headerValue+"b"); + introspector.setK2TracingData(headerValue); + } + + private void verifyHeaders(String headerValue, Map headers) { + Assert.assertTrue(String.format("Missing K2 header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue+"a", headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); + Assert.assertTrue(String.format("Missing K2 header: %s", GenericHelper.CSEC_PARENT_ID), headers.containsKey(GenericHelper.CSEC_PARENT_ID)); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", GenericHelper.CSEC_PARENT_ID), headerValue+"b", headers.get(GenericHelper.CSEC_PARENT_ID)); + Assert.assertTrue(String.format("Missing K2 header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", + headerValue), headers.get( + ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + } + @Trace(dispatcher = true) public void callExecute() throws URISyntaxException, IOException { try (CloseableHttpClient httpclient = HttpClients.createDefault()) { diff --git a/instrumentation-security/httpclient-5.0/build.gradle b/instrumentation-security/httpclient-5.0/build.gradle new file mode 100644 index 000000000..94cb7437b --- /dev/null +++ b/instrumentation-security/httpclient-5.0/build.gradle @@ -0,0 +1,22 @@ + +dependencies { + implementation(project(":newrelic-security-api")) + implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}") + implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}") + implementation("org.apache.httpcomponents.client5:httpclient5:5.2.1") +} + +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.httpclient-5.0' } +} + +verifyInstrumentation { + passesOnly ('org.apache.httpcomponents.client5:httpclient5:[5.0,)') + excludeRegex '.*alpha.*' + excludeRegex '.*beta.*' +} + +site { + title 'Apache Httpclient' + type 'Messaging' +} \ No newline at end of file diff --git a/instrumentation-security/httpclient-5.0/src/main/java/com/newrelic/agent/security/instrumentation/httpclient50/BasicRequestProducer_Instrumentation.java b/instrumentation-security/httpclient-5.0/src/main/java/com/newrelic/agent/security/instrumentation/httpclient50/BasicRequestProducer_Instrumentation.java new file mode 100644 index 000000000..2803b7cf1 --- /dev/null +++ b/instrumentation-security/httpclient-5.0/src/main/java/com/newrelic/agent/security/instrumentation/httpclient50/BasicRequestProducer_Instrumentation.java @@ -0,0 +1,24 @@ +/* + * + * * Copyright 2023 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.security.instrumentation.httpclient50; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.nio.AsyncEntityProducer; + +import static com.newrelic.agent.security.instrumentation.httpclient50.SecurityHelper.APACHE5_ASYNC_REQUEST_PRODUCER; + +@Weave(type=MatchType.BaseClass, originalName = "org.apache.hc.core5.http.nio.support.BasicRequestProducer") +public class BasicRequestProducer_Instrumentation { + + public BasicRequestProducer_Instrumentation(final HttpRequest request, final AsyncEntityProducer dataProducer) { + NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(APACHE5_ASYNC_REQUEST_PRODUCER+this.hashCode(), request); + } +} diff --git a/instrumentation-security/httpclient-5.0/src/main/java/com/newrelic/agent/security/instrumentation/httpclient50/HttpAsyncClient_Instrumentation.java b/instrumentation-security/httpclient-5.0/src/main/java/com/newrelic/agent/security/instrumentation/httpclient50/HttpAsyncClient_Instrumentation.java new file mode 100644 index 000000000..697d45fb9 --- /dev/null +++ b/instrumentation-security/httpclient-5.0/src/main/java/com/newrelic/agent/security/instrumentation/httpclient50/HttpAsyncClient_Instrumentation.java @@ -0,0 +1,76 @@ +/* + * + * * Copyright 2023 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.security.instrumentation.httpclient50; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.nio.AsyncPushConsumer; +import org.apache.hc.core5.http.nio.AsyncRequestProducer; +import org.apache.hc.core5.http.nio.AsyncResponseConsumer; +import org.apache.hc.core5.http.nio.HandlerFactory; +import org.apache.hc.core5.http.protocol.HttpContext; + +import java.net.URISyntaxException; +import java.util.concurrent.Future; + +import static com.newrelic.agent.security.instrumentation.httpclient50.SecurityHelper.APACHE5_ASYNC_REQUEST_PRODUCER; + +@Weave(type = MatchType.Interface, originalName = "org.apache.hc.client5.http.async.HttpAsyncClient") +public class HttpAsyncClient_Instrumentation { + + public Future execute( + AsyncRequestProducer requestProducer, + AsyncResponseConsumer responseConsumer, + HandlerFactory pushHandlerFactory, + HttpContext context, + FutureCallback callback) { + HttpRequest request = NewRelicSecurity.getAgent().getSecurityMetaData().getCustomAttribute(APACHE5_ASYNC_REQUEST_PRODUCER+requestProducer.hashCode(), HttpRequest.class); + + boolean isLockAcquired = acquireLockIfPossible(); + AbstractOperation operation = null; + // Preprocess Phase + if (isLockAcquired) { + try { + operation = SecurityHelper.preprocessSecurityHook(request, request.getUri().toString(), this.getClass().getName(), SecurityHelper.METHOD_NAME_EXECUTE); + } catch (URISyntaxException ignored) { + } + } + Future returnObj = null; + // Actual Call + try { + returnObj = Weaver.callOriginal(); + } finally { + if (isLockAcquired) { + releaseLock(); + } + } + SecurityHelper.registerExitOperation(isLockAcquired, operation); + return returnObj; + } + + private void releaseLock() { + try { + GenericHelper.releaseLock(SecurityHelper.NR_SEC_CUSTOM_ATTRIB_NAME, this.hashCode()); + } catch (Throwable ignored) { + } + } + + private boolean acquireLockIfPossible() { + try { + return GenericHelper.acquireLockIfPossible(SecurityHelper.NR_SEC_CUSTOM_ATTRIB_NAME, this.hashCode()); + } catch (Throwable ignored) { + } + return false; + } +} diff --git a/instrumentation-security/httpclient-5.0/src/main/java/com/newrelic/agent/security/instrumentation/httpclient50/HttpClient_Instrumentation.java b/instrumentation-security/httpclient-5.0/src/main/java/com/newrelic/agent/security/instrumentation/httpclient50/HttpClient_Instrumentation.java new file mode 100644 index 000000000..6954a9991 --- /dev/null +++ b/instrumentation-security/httpclient-5.0/src/main/java/com/newrelic/agent/security/instrumentation/httpclient50/HttpClient_Instrumentation.java @@ -0,0 +1,218 @@ +/* + * + * * Copyright 2023 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.security.instrumentation.httpclient50; + +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.io.HttpClientResponseHandler; +import org.apache.hc.core5.http.protocol.HttpContext; + +import java.net.URI; +import java.net.URISyntaxException; + +@Weave(type = MatchType.Interface, originalName = "org.apache.hc.client5.http.classic.HttpClient") +public class HttpClient_Instrumentation { + + public HttpResponse execute(ClassicHttpRequest request) throws Exception { + boolean isLockAcquired = acquireLockIfPossible(); + AbstractOperation operation = null; + // Preprocess Phase + if (isLockAcquired) { + operation = SecurityHelper.preprocessSecurityHook(request, request.getUri().toString(), this.getClass().getName(), SecurityHelper.METHOD_NAME_EXECUTE); + } + HttpResponse returnObj = null; + // Actual Call + try { + returnObj = Weaver.callOriginal(); + } finally { + if (isLockAcquired) { + releaseLock(); + } + } + SecurityHelper.registerExitOperation(isLockAcquired, operation); + return returnObj; + } + + public HttpResponse execute(ClassicHttpRequest request, HttpContext context) throws Exception { + boolean isLockAcquired = acquireLockIfPossible(); + AbstractOperation operation = null; + // Preprocess Phase + if (isLockAcquired) { + operation = SecurityHelper.preprocessSecurityHook(request, request.getUri().toString(), this.getClass().getName(), SecurityHelper.METHOD_NAME_EXECUTE); + } + HttpResponse returnObj = null; + // Actual Call + try { + returnObj = Weaver.callOriginal(); + } finally { + if (isLockAcquired) { + releaseLock(); + } + } + SecurityHelper.registerExitOperation(isLockAcquired, operation); + return returnObj; + } + + public ClassicHttpResponse execute(HttpHost target, ClassicHttpRequest request) throws Exception { + boolean isLockAcquired = acquireLockIfPossible(); + AbstractOperation operation = null; + // Preprocess Phase + if (isLockAcquired) { + String actualURI = getUri(target, request).toString(); + operation = SecurityHelper.preprocessSecurityHook(request, actualURI, this.getClass().getName(), SecurityHelper.METHOD_NAME_EXECUTE); + } + ClassicHttpResponse returnObj = null; + // Actual Call + try { + returnObj = Weaver.callOriginal(); + } finally { + if (isLockAcquired) { + releaseLock(); + } + } + SecurityHelper.registerExitOperation(isLockAcquired, operation); + return returnObj; + } + + public HttpResponse execute(HttpHost target, ClassicHttpRequest request, HttpContext context) throws Exception { + boolean isLockAcquired = acquireLockIfPossible(); + AbstractOperation operation = null; + // Preprocess Phase + if (isLockAcquired) { + String actualURI = getUri(target, request).toString(); + operation = SecurityHelper.preprocessSecurityHook(request, actualURI, this.getClass().getName(), SecurityHelper.METHOD_NAME_EXECUTE); + } + HttpResponse returnObj = null; + // Actual Call + try { + returnObj = Weaver.callOriginal(); + } finally { + if (isLockAcquired) { + releaseLock(); + } + } + SecurityHelper.registerExitOperation(isLockAcquired, operation); + return returnObj; + } + + public T execute(ClassicHttpRequest request, HttpClientResponseHandler responseHandler) + throws Exception { + boolean isLockAcquired = acquireLockIfPossible(); + AbstractOperation operation = null; + // Preprocess Phase + if (isLockAcquired) { + operation = SecurityHelper.preprocessSecurityHook(request, request.getUri().toString(), this.getClass().getName(), SecurityHelper.METHOD_NAME_EXECUTE); + } + T returnObj = null; + // Actual Call + try { + returnObj = Weaver.callOriginal(); + } finally { + if (isLockAcquired) { + releaseLock(); + } + } + SecurityHelper.registerExitOperation(isLockAcquired, operation); + return returnObj; + } + + public T execute(ClassicHttpRequest request, HttpContext context, HttpClientResponseHandler responseHandler) + throws Exception { + boolean isLockAcquired = acquireLockIfPossible(); + AbstractOperation operation = null; + // Preprocess Phase + if (isLockAcquired) { + operation = SecurityHelper.preprocessSecurityHook(request, request.getUri().toString(), this.getClass().getName(), SecurityHelper.METHOD_NAME_EXECUTE); + } + T returnObj = null; + // Actual Call + try { + returnObj = Weaver.callOriginal(); + } finally { + if (isLockAcquired) { + releaseLock(); + } + } + SecurityHelper.registerExitOperation(isLockAcquired, operation); + return returnObj; + } + + public T execute(HttpHost target, ClassicHttpRequest request, HttpClientResponseHandler responseHandler) + throws Exception { + boolean isLockAcquired = acquireLockIfPossible(); + AbstractOperation operation = null; + // Preprocess Phase + if (isLockAcquired) { + String actualURI = getUri(target, request).toString(); + operation = SecurityHelper.preprocessSecurityHook(request, actualURI, this.getClass().getName(), SecurityHelper.METHOD_NAME_EXECUTE); + } + T returnObj = null; + // Actual Call + try { + returnObj = Weaver.callOriginal(); + } finally { + if (isLockAcquired) { + releaseLock(); + } + } + SecurityHelper.registerExitOperation(isLockAcquired, operation); + return returnObj; + } + + public T execute(HttpHost target, ClassicHttpRequest request, HttpContext context, + HttpClientResponseHandler responseHandler) throws Exception { + boolean isLockAcquired = acquireLockIfPossible(); + AbstractOperation operation = null; + // Preprocess Phase + if (isLockAcquired) { + String actualURI = getUri(target, request).toString(); + operation = SecurityHelper.preprocessSecurityHook(request, actualURI, this.getClass().getName(), SecurityHelper.METHOD_NAME_EXECUTE); + } + T returnObj = null; + // Actual Call + try { + returnObj = Weaver.callOriginal(); + } finally { + if (isLockAcquired) { + releaseLock(); + } + } + SecurityHelper.registerExitOperation(isLockAcquired, operation); + return returnObj; + } + + private static URI getUri(HttpHost target, HttpRequest request) throws URISyntaxException { + URI requestURI = new URI(request.getRequestUri()); + String scheme = requestURI.getScheme() == null ? target.getSchemeName() : requestURI.getScheme(); + return new URI(scheme, null, target.getHostName(), target.getPort(), requestURI.getPath(), null, null); + } + + private void releaseLock() { + try { + GenericHelper.releaseLock(SecurityHelper.NR_SEC_CUSTOM_ATTRIB_NAME, this.hashCode()); + } catch (Throwable ignored) { + } + } + + private boolean acquireLockIfPossible() { + try { + return GenericHelper.acquireLockIfPossible(SecurityHelper.NR_SEC_CUSTOM_ATTRIB_NAME, this.hashCode()); + } catch (Throwable ignored) { + } + return false; + } + +} diff --git a/instrumentation-security/httpclient-5.0/src/main/java/com/newrelic/agent/security/instrumentation/httpclient50/SecurityHelper.java b/instrumentation-security/httpclient-5.0/src/main/java/com/newrelic/agent/security/instrumentation/httpclient50/SecurityHelper.java new file mode 100644 index 000000000..7b643cc53 --- /dev/null +++ b/instrumentation-security/httpclient-5.0/src/main/java/com/newrelic/agent/security/instrumentation/httpclient50/SecurityHelper.java @@ -0,0 +1,73 @@ +package com.newrelic.agent.security.instrumentation.httpclient50; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.SecurityMetaData; +import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityException; +import com.newrelic.api.agent.security.schema.operation.SSRFOperation; +import com.newrelic.api.agent.security.utils.SSRFUtils; +import org.apache.hc.core5.http.HttpRequest; + +public class SecurityHelper { + + public static final String METHOD_NAME_EXECUTE = "execute"; + + public static final String NR_SEC_CUSTOM_ATTRIB_NAME = "SSRF_OPERATION_LOCK_APACHE5-"; + public static final String APACHE5_ASYNC_REQUEST_PRODUCER = "APACHE5_ASYNC_REQUEST_PRODUCER_"; + + public static void registerExitOperation(boolean isProcessingAllowed, AbstractOperation operation) { + try { + if (operation == null || !isProcessingAllowed || !NewRelicSecurity.isHookProcessingActive() || NewRelicSecurity.getAgent().getSecurityMetaData().getRequest().isEmpty() + ) { + return; + } + NewRelicSecurity.getAgent().registerExitEvent(operation); + } catch (Throwable ignored) { + } + } + + public static AbstractOperation preprocessSecurityHook(HttpRequest request, String uri, String className, String methodName) { + try { + SecurityMetaData securityMetaData = NewRelicSecurity.getAgent().getSecurityMetaData(); + if (!NewRelicSecurity.isHookProcessingActive() || securityMetaData.getRequest().isEmpty() + ) { + return null; + } + + // Add Security IAST header + String iastHeader = NewRelicSecurity.getAgent().getSecurityMetaData().getFuzzRequestIdentifier().getRaw(); + if (iastHeader != null && !iastHeader.trim().isEmpty()) { + request.setHeader(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID, iastHeader); + } + + String csecParentId = getParentId(); + if(csecParentId!= null && !csecParentId.isEmpty()){ + request.setHeader(GenericHelper.CSEC_PARENT_ID, csecParentId); + } + + SSRFOperation operation = new SSRFOperation(uri, className, methodName); + try { + NewRelicSecurity.getAgent().registerOperation(operation); + } finally { + if (operation.getApiID() != null && !operation.getApiID().trim().isEmpty() && + operation.getExecutionId() != null && !operation.getExecutionId().trim().isEmpty()) { + // Add Security distributed tracing header + request.setHeader(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER, SSRFUtils.generateTracingHeaderValue(securityMetaData.getTracingHeaderValue(), operation.getApiID(), operation.getExecutionId(), NewRelicSecurity.getAgent().getAgentUUID())); + } + } + return operation; + } catch (Throwable e) { + if (e instanceof NewRelicSecurityException) { + e.printStackTrace(); + throw e; + } + } + return null; + } + + public static String getParentId(){ + return NewRelicSecurity.getAgent().getSecurityMetaData().getCustomAttribute(GenericHelper.CSEC_PARENT_ID, String.class); + } +} diff --git a/instrumentation-security/httpclient-5.0/src/test/java/com/nr/agent/security/instrumentation/httpclient5/HttpAsyncClientTest.java b/instrumentation-security/httpclient-5.0/src/test/java/com/nr/agent/security/instrumentation/httpclient5/HttpAsyncClientTest.java new file mode 100644 index 000000000..485f2b5b3 --- /dev/null +++ b/instrumentation-security/httpclient-5.0/src/test/java/com/nr/agent/security/instrumentation/httpclient5/HttpAsyncClientTest.java @@ -0,0 +1,186 @@ +package com.nr.agent.security.instrumentation.httpclient5; + +import com.newrelic.agent.security.introspec.InstrumentationTestConfig; +import com.newrelic.agent.security.introspec.SecurityInstrumentationTestRunner; +import com.newrelic.agent.security.introspec.SecurityIntrospector; +import com.newrelic.agent.security.introspec.internal.HttpServerRule; +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; +import com.newrelic.api.agent.security.schema.operation.SSRFOperation; +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.HttpAsyncClients; +import org.apache.hc.core5.http.Message; +import org.apache.hc.core5.http.nio.entity.BasicAsyncEntityConsumer; +import org.apache.hc.core5.http.nio.entity.BasicAsyncEntityProducer; +import org.apache.hc.core5.http.nio.support.BasicRequestProducer; +import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; +import org.apache.hc.core5.http.protocol.BasicHttpContext; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.Future; + +@RunWith(SecurityInstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = "com.newrelic.agent.security.instrumentation.httpclient50") +public class HttpAsyncClientTest { + @ClassRule + public static HttpServerRule server = new HttpServerRule(); + + @Test + public void testExecute() throws URISyntaxException { + String headerValue = String.valueOf(UUID.randomUUID()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + + callExecute(); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + verifyHeaders(headerValue, headers); + } + + @Test + public void testExecute1() throws URISyntaxException { + + String headerValue = String.valueOf(UUID.randomUUID()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + callExecute1(); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + verifyHeaders(headerValue, headers); + } + + @Test + public void testExecute2() throws URISyntaxException { + + String headerValue = String.valueOf(UUID.randomUUID()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + callExecute2(); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + verifyHeaders(headerValue, headers); + + } + + @Test + public void testExecute3() throws URISyntaxException { + + String headerValue = String.valueOf(UUID.randomUUID()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + callExecute3(); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + verifyHeaders(headerValue, headers); + + } + + private void setCSECHeaders(String headerValue, SecurityIntrospector introspector) { + introspector.setK2FuzzRequestId(headerValue+"a"); + introspector.setK2ParentId(headerValue+"b"); + introspector.setK2TracingData(headerValue); + } + + private void verifyHeaders(String headerValue, Map headers) { + Assert.assertTrue(String.format("Missing K2 header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue+"a", headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); + Assert.assertTrue(String.format("Missing K2 header: %s", GenericHelper.CSEC_PARENT_ID), headers.containsKey(GenericHelper.CSEC_PARENT_ID)); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", GenericHelper.CSEC_PARENT_ID), headerValue+"b", headers.get(GenericHelper.CSEC_PARENT_ID)); + Assert.assertTrue(String.format("Missing K2 header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", + headerValue), headers.get( + ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + } + + @Trace(dispatcher = true) + private void callExecute() { + try (CloseableHttpAsyncClient httpAsyncClient = HttpAsyncClients.createDefault()) { + httpAsyncClient.start(); + SimpleHttpRequest request = SimpleRequestBuilder.get(server.getEndPoint()).build(); + Future future = httpAsyncClient.execute(request, null); + SimpleHttpResponse response = future.get(); + System.out.println("response: " + response.getBody()); + } catch (Exception ignored) { + } + } + + @Trace(dispatcher = true) + private void callExecute1() { + try (CloseableHttpAsyncClient httpAsyncClient = HttpAsyncClients.createDefault()) { + httpAsyncClient.start(); + SimpleHttpRequest request = SimpleRequestBuilder.get(server.getEndPoint()).build(); + HttpContext httpContext = new BasicHttpContext(); + Future future = httpAsyncClient.execute(request, httpContext, null); + SimpleHttpResponse response = future.get(); + System.out.println("response: " + response.getBody()); + } catch (Exception ignored) { + } + } + + @Trace(dispatcher = true) + private void callExecute2() { + try (CloseableHttpAsyncClient httpAsyncClient = HttpAsyncClients.createDefault()) { + httpAsyncClient.start(); + SimpleHttpRequest request = SimpleRequestBuilder.get(server.getEndPoint()).build(); + Future future = httpAsyncClient.execute(new BasicRequestProducer(request, new BasicAsyncEntityProducer("test data")), new BasicResponseConsumer(new BasicAsyncEntityConsumer()), null); + SimpleHttpResponse response = future.get(); + System.out.println("response: " + response.getBody()); + } catch (Exception ignored) { + } + } + + @Trace(dispatcher = true) + public void callExecute3() { + try (CloseableHttpAsyncClient httpAsyncClient = HttpAsyncClients.createDefault()) { + httpAsyncClient.start(); + SimpleHttpRequest request = SimpleRequestBuilder.get(server.getEndPoint()).build(); + HttpContext httpContext = new BasicHttpContext(); + Future future = httpAsyncClient.execute(new BasicRequestProducer(request, new BasicAsyncEntityProducer("test data")), new BasicResponseConsumer(new BasicAsyncEntityConsumer()), httpContext, null); + Message response = future.get(); + System.out.println("response: " + response.getBody()); + } catch (Exception ignored) { + } + } +} diff --git a/instrumentation-security/httpclient-5.0/src/test/java/com/nr/agent/security/instrumentation/httpclient5/HttpClientTest.java b/instrumentation-security/httpclient-5.0/src/test/java/com/nr/agent/security/instrumentation/httpclient5/HttpClientTest.java new file mode 100644 index 000000000..b9c33d1ab --- /dev/null +++ b/instrumentation-security/httpclient-5.0/src/test/java/com/nr/agent/security/instrumentation/httpclient5/HttpClientTest.java @@ -0,0 +1,308 @@ +package com.nr.agent.security.instrumentation.httpclient5; + +import com.newrelic.agent.security.introspec.InstrumentationTestConfig; +import com.newrelic.agent.security.introspec.SecurityInstrumentationTestRunner; +import com.newrelic.agent.security.introspec.SecurityIntrospector; +import com.newrelic.agent.security.introspec.internal.HttpServerRule; +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; +import com.newrelic.api.agent.security.schema.operation.SSRFOperation; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.impl.classic.BasicHttpClientResponseHandler; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.io.HttpClientResponseHandler; +import org.apache.hc.core5.http.protocol.BasicHttpContext; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@RunWith(SecurityInstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = "com.newrelic.agent.security.instrumentation.httpclient50") +public class HttpClientTest { + @ClassRule + public static HttpServerRule server = new HttpServerRule(); + + @Test + public void testExecute() throws URISyntaxException, IOException { + String headerValue = String.valueOf(UUID.randomUUID()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + + callExecute(); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + verifyHeaders(headerValue, headers); + } + + @Test + public void testExecute1() throws URISyntaxException, IOException { + + String headerValue = String.valueOf(UUID.randomUUID()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + callExecute1(); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + verifyHeaders(headerValue, headers); + } + + @Test + public void testExecute2() throws URISyntaxException, IOException { + + String headerValue = String.valueOf(UUID.randomUUID()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + callExecute2(); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + verifyHeaders(headerValue, headers); + + } + + @Test + public void testExecute3() throws URISyntaxException, IOException { + + String headerValue = String.valueOf(UUID.randomUUID()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + callExecute3(); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + verifyHeaders(headerValue, headers); + + } + + @Test + public void testExecute4() throws Exception { + + + String headerValue = String.valueOf(UUID.randomUUID()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + callExecute4(); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + verifyHeaders(headerValue, headers); + + } + + @Test + public void testExecute5() throws Exception { + + + String headerValue = String.valueOf(UUID.randomUUID()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + callExecute5(); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + verifyHeaders(headerValue, headers); + + } + + @Test + public void testExecute6() throws Exception { + + + String headerValue = String.valueOf(UUID.randomUUID()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + callExecute6(); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + verifyHeaders(headerValue, headers); + + } + + @Test + public void testExecute7() throws Exception { + + + String headerValue = String.valueOf(UUID.randomUUID()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + setCSECHeaders(headerValue, introspector); + callExecute7(); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + SSRFOperation operation = (SSRFOperation) operations.get(0); + Map headers = server.getHeaders(); + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); + verifyHeaders(headerValue, headers); + + } + + private void setCSECHeaders(String headerValue, SecurityIntrospector introspector) { + introspector.setK2FuzzRequestId(headerValue+"a"); + introspector.setK2ParentId(headerValue+"b"); + introspector.setK2TracingData(headerValue); + } + + private void verifyHeaders(String headerValue, Map headers) { + Assert.assertTrue(String.format("Missing K2 header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue+"a", headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); + Assert.assertTrue(String.format("Missing K2 header: %s", GenericHelper.CSEC_PARENT_ID), headers.containsKey(GenericHelper.CSEC_PARENT_ID)); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", GenericHelper.CSEC_PARENT_ID), headerValue+"b", headers.get(GenericHelper.CSEC_PARENT_ID)); + Assert.assertTrue(String.format("Missing K2 header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", + headerValue), headers.get( + ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + } + + @Trace(dispatcher = true) + public void callExecute() throws URISyntaxException, IOException { + try (CloseableHttpClient httpclient = HttpClients.createDefault()) { + HttpGet httpGet = new HttpGet(server.getEndPoint().toString()); + httpclient.execute(httpGet); + Thread.sleep(100); + } catch (InterruptedException e) { + } + } + + @Trace(dispatcher = true) + public void callExecute1() throws URISyntaxException, IOException { + try (CloseableHttpClient httpclient = HttpClients.createDefault()) { + HttpGet httpGet = new HttpGet(server.getEndPoint().toString()); + HttpContext httpContext = new BasicHttpContext(); + httpclient.execute(httpGet, httpContext); + Thread.sleep(100); + } catch (InterruptedException e) { + } + } + + @Trace(dispatcher = true) + public void callExecute2() throws URISyntaxException, IOException { + try (CloseableHttpClient httpclient = HttpClients.createDefault()) { + HttpHost httpHost = new HttpHost(server.getEndPoint().getScheme(), server.getEndPoint().getHost(), server.getEndPoint().getPort()); + HttpGet httpGet = new HttpGet(server.getEndPoint().toString()); + httpclient.execute(httpHost, httpGet); + Thread.sleep(100); + } catch (InterruptedException e) { + } + } + + @Trace(dispatcher = true) + public void callExecute3() throws URISyntaxException, IOException { + try(CloseableHttpClient httpclient = HttpClients.createDefault()) { + HttpHost httpHost = new HttpHost(server.getEndPoint().getScheme(), server.getEndPoint().getHost(), server.getEndPoint().getPort()); + HttpGet httpGet = new HttpGet(server.getEndPoint().toString()); + HttpContext httpContext = new BasicHttpContext(); + httpclient.execute(httpHost, httpGet, httpContext); + Thread.sleep(100); + } catch (InterruptedException e) { + } + } + + @Trace(dispatcher = true) + public void callExecute4() throws Exception { + try (CloseableHttpClient httpclient = HttpClients.createDefault()) { + HttpGet httpGet = new HttpGet(server.getEndPoint().toString()); + HttpClientResponseHandler responseHandler = new BasicHttpClientResponseHandler(); + httpclient.execute(httpGet, responseHandler); + Thread.sleep(100); + } catch (InterruptedException e) { + } + } + + @Trace(dispatcher = true) + public void callExecute5() throws Exception { + try(CloseableHttpClient httpclient = HttpClients.createDefault()) { + HttpGet httpGet = new HttpGet(server.getEndPoint().toString()); + HttpClientResponseHandler responseHandler = new BasicHttpClientResponseHandler(); + HttpContext httpContext = new BasicHttpContext(); + httpclient.execute(httpGet, httpContext, responseHandler); + Thread.sleep(100); + } catch (InterruptedException e) { + } + } + + @Trace(dispatcher = true) + public void callExecute6() throws Exception { + try(CloseableHttpClient httpclient = HttpClients.createDefault()) { + HttpGet httpGet = new HttpGet(server.getEndPoint().toString()); + HttpClientResponseHandler responseHandler = new BasicHttpClientResponseHandler(); + HttpHost httpHost = new HttpHost(server.getEndPoint().getScheme(), server.getEndPoint().getHost(), server.getEndPoint().getPort()); + httpclient.execute(httpHost, httpGet, responseHandler); + Thread.sleep(100); + } catch (InterruptedException e) { + } + } + + @Trace(dispatcher = true) + public void callExecute7() throws Exception { + try(CloseableHttpClient httpclient = HttpClients.createDefault()) { + HttpGet httpGet = new HttpGet(server.getEndPoint().toString()); + HttpClientResponseHandler responseHandler = new BasicHttpClientResponseHandler(); + HttpHost httpHost = new HttpHost(server.getEndPoint().getScheme(), server.getEndPoint().getHost(), server.getEndPoint().getPort()); + HttpContext httpContext = new BasicHttpContext(); + httpclient.execute(httpHost, httpGet, httpContext, responseHandler); + Thread.sleep(100); + } catch (InterruptedException e) { + } + } +} diff --git a/instrumentation-security/httpclient-5.0/src/test/resources/distributed_tracing.yml b/instrumentation-security/httpclient-5.0/src/test/resources/distributed_tracing.yml new file mode 100644 index 000000000..f2f195c17 --- /dev/null +++ b/instrumentation-security/httpclient-5.0/src/test/resources/distributed_tracing.yml @@ -0,0 +1,3 @@ +common: &default_settings + distributed_tracing: + enabled: true diff --git a/instrumentation-security/httpclient-5.0/src/test/resources/spans.yml b/instrumentation-security/httpclient-5.0/src/test/resources/spans.yml new file mode 100644 index 000000000..e84c276ec --- /dev/null +++ b/instrumentation-security/httpclient-5.0/src/test/resources/spans.yml @@ -0,0 +1,5 @@ +common: &default_settings + distributed_tracing: + enabled: true + span_events: + enabled: true diff --git a/instrumentation-security/httpclient-jdk11/src/test/java/com/nr/agent/security/instrumentation/httpclientJDK11/HttpClientTest.java b/instrumentation-security/httpclient-jdk11/src/test/java/com/nr/agent/security/instrumentation/httpclientJDK11/HttpClientTest.java index a9eee6ba5..648698150 100644 --- a/instrumentation-security/httpclient-jdk11/src/test/java/com/nr/agent/security/instrumentation/httpclientJDK11/HttpClientTest.java +++ b/instrumentation-security/httpclient-jdk11/src/test/java/com/nr/agent/security/instrumentation/httpclientJDK11/HttpClientTest.java @@ -5,6 +5,7 @@ import com.newrelic.agent.security.introspec.SecurityIntrospector; import com.newrelic.agent.security.introspec.internal.HttpServerRule; import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; import com.newrelic.api.agent.security.schema.AbstractOperation; import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; @@ -40,8 +41,7 @@ public void testSendAsync() throws Exception { String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); - introspector.setK2FuzzRequestId(headerValue); - introspector.setK2TracingData(headerValue); + setCSECHeaders(headerValue, introspector); send(); @@ -53,14 +53,7 @@ public void testSendAsync() throws Exception { Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed method name.", "sendAsync", operation.getMethodName()); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue, headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); - Assert.assertEquals( - String.format("Invalid CSEC header value for: %s", - ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;",headerValue), - headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase()) - ); + verifyHeaders(headerValue, headers); } @Test @@ -68,8 +61,7 @@ public void testSendAsync1() throws Exception { String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); - introspector.setK2FuzzRequestId(headerValue); - introspector.setK2TracingData(headerValue); + setCSECHeaders(headerValue, introspector); sendAsync(); @@ -81,14 +73,7 @@ public void testSendAsync1() throws Exception { Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed method name.", "sendAsync", operation.getMethodName()); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue, headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); - Assert.assertEquals( - String.format("Invalid CSEC header value for: %s", - ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;",headerValue), - headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase()) - ); + verifyHeaders(headerValue, headers); } @Test @@ -96,8 +81,7 @@ public void testSendAsync2() throws Exception { String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); - introspector.setK2FuzzRequestId(headerValue); - introspector.setK2TracingData(headerValue); + setCSECHeaders(headerValue, introspector); sendAsync1(); @@ -109,14 +93,24 @@ public void testSendAsync2() throws Exception { Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed method name.", "sendAsync", operation.getMethodName()); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue, headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); - Assert.assertEquals( - String.format("Invalid CSEC header value for: %s", - ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;",headerValue), - headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase()) - ); + verifyHeaders(headerValue, headers); + } + + private void setCSECHeaders(String headerValue, SecurityIntrospector introspector) { + introspector.setK2FuzzRequestId(headerValue+"a"); + introspector.setK2ParentId(headerValue+"b"); + introspector.setK2TracingData(headerValue); + } + + private void verifyHeaders(String headerValue, Map headers) { + Assert.assertTrue(String.format("Missing K2 header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue+"a", headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); + Assert.assertTrue(String.format("Missing K2 header: %s", GenericHelper.CSEC_PARENT_ID), headers.containsKey(GenericHelper.CSEC_PARENT_ID)); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", GenericHelper.CSEC_PARENT_ID), headerValue+"b", headers.get(GenericHelper.CSEC_PARENT_ID)); + Assert.assertTrue(String.format("Missing K2 header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", + headerValue), headers.get( + ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); } @Trace(dispatcher = true) diff --git a/instrumentation-security/java-io-inputstream-jdk8/src/main/java/java/io/InputStream_Instrumentation.java b/instrumentation-security/java-io-inputstream-jdk8/src/main/java/java/io/InputStream_Instrumentation.java index fd61a2aa1..4e54fbbdd 100644 --- a/instrumentation-security/java-io-inputstream-jdk8/src/main/java/java/io/InputStream_Instrumentation.java +++ b/instrumentation-security/java-io-inputstream-jdk8/src/main/java/java/io/InputStream_Instrumentation.java @@ -9,7 +9,7 @@ import com.newrelic.api.agent.security.NewRelicSecurity; import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; import com.newrelic.api.agent.weaver.*; -import com.newrelic.agent.security.instrumentation.javaio.InputStreamHelper; +import com.newrelic.api.agent.security.instrumentation.helpers.InputStreamHelper; @Weave(type = MatchType.BaseClass, originalName = "java.io.InputStream") public abstract class InputStream_Instrumentation { diff --git a/instrumentation-security/java-io-inputstream-jdk9/src/main/java/com/newrelic/agent/security/instrumentation/javaio/InputStreamHelper.java b/instrumentation-security/java-io-inputstream-jdk9/src/main/java/com/newrelic/agent/security/instrumentation/javaio/InputStreamHelper.java deleted file mode 100644 index 396466e06..000000000 --- a/instrumentation-security/java-io-inputstream-jdk9/src/main/java/com/newrelic/agent/security/instrumentation/javaio/InputStreamHelper.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.newrelic.agent.security.instrumentation.javaio; - -import com.newrelic.api.agent.security.NewRelicSecurity; - -import java.util.Set; - -public class InputStreamHelper { - - - private static final String REQUEST_INPUTSTREAM_HASH = "REQUEST_INPUTSTREAM_HASH"; - - public static final String NR_SEC_CUSTOM_ATTRIB_NAME = "SERVLET_IS_OPERATION_LOCK-"; - - public static final String LF = "\n"; - - public static Boolean processRequestInputStreamHookData(Integer inputStreamHash) { - try { - if(NewRelicSecurity.isHookProcessingActive() && NewRelicSecurity.getAgent().getSecurityMetaData()!= null) { - Set hashSet = NewRelicSecurity.getAgent().getSecurityMetaData().getCustomAttribute(REQUEST_INPUTSTREAM_HASH, Set.class); - if(hashSet != null){ - return hashSet.contains(inputStreamHash); - } - } - } catch (Throwable ignored){} - return false; - } - -} diff --git a/instrumentation-security/java-io-inputstream-jdk9/src/main/java/com/newrelic/agent/security/instrumentation/javaio/io/InputStream_Instrumentation.java b/instrumentation-security/java-io-inputstream-jdk9/src/main/java/com/newrelic/agent/security/instrumentation/javaio/io/InputStream_Instrumentation.java index 6392adcea..3560cc4af 100644 --- a/instrumentation-security/java-io-inputstream-jdk9/src/main/java/com/newrelic/agent/security/instrumentation/javaio/io/InputStream_Instrumentation.java +++ b/instrumentation-security/java-io-inputstream-jdk9/src/main/java/com/newrelic/agent/security/instrumentation/javaio/io/InputStream_Instrumentation.java @@ -8,8 +8,8 @@ package com.newrelic.agent.security.instrumentation.javaio.io; import com.newrelic.api.agent.security.NewRelicSecurity; import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.security.instrumentation.helpers.InputStreamHelper; import com.newrelic.api.agent.weaver.*; -import com.newrelic.agent.security.instrumentation.javaio.InputStreamHelper; import java.io.IOException; @Weave(type = MatchType.BaseClass, originalName = "java.io.InputStream") diff --git a/instrumentation-security/jcache-1.0.0/build.gradle b/instrumentation-security/jcache-1.0.0/build.gradle new file mode 100644 index 000000000..486a7b10b --- /dev/null +++ b/instrumentation-security/jcache-1.0.0/build.gradle @@ -0,0 +1,21 @@ + +dependencies { + implementation(project(":newrelic-security-api")) + implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}") + implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}") + implementation("javax.cache:cache-api:1.0.0") + testImplementation("com.hazelcast:hazelcast:4.2.8") +} + +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.jcache-1.0.0' } +} + +verifyInstrumentation { + passes 'javax.cache:cache-api:[1.0.0,)' +} + +site { + title 'JCache API' + type 'Framework' +} \ No newline at end of file diff --git a/instrumentation-security/jcache-1.0.0/src/main/java/com/newrelic/agent/security/instrumentation/jcache_1_0_0/JCacheHelper.java b/instrumentation-security/jcache-1.0.0/src/main/java/com/newrelic/agent/security/instrumentation/jcache_1_0_0/JCacheHelper.java new file mode 100644 index 000000000..5ebbbdf87 --- /dev/null +++ b/instrumentation-security/jcache-1.0.0/src/main/java/com/newrelic/agent/security/instrumentation/jcache_1_0_0/JCacheHelper.java @@ -0,0 +1,56 @@ +package com.newrelic.agent.security.instrumentation.jcache_1_0_0; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityException; +import com.newrelic.api.agent.security.schema.operation.JCacheOperation; + +import java.util.List; + +public class JCacheHelper { + public static final String READ = "read"; + public static final String WRITE = "write"; + public static final String DELETE = "delete"; + public static final String UPDATE = "update"; + public static final String NR_SEC_CUSTOM_ATTRIB_NAME = "JCACHE-OPERATION-LOCK-"; + + public static AbstractOperation preprocessSecurityHook(String command, List args, String klass, String method) { + try { + if (!NewRelicSecurity.isHookProcessingActive() || NewRelicSecurity.getAgent().getSecurityMetaData().getRequest().isEmpty()){ + return null; + } + JCacheOperation operation = new JCacheOperation(klass, method, command, args); + NewRelicSecurity.getAgent().registerOperation(operation); + return operation; + } catch (Throwable e) { + if (e instanceof NewRelicSecurityException) { + throw e; + } + } + return null; + } + + public static void registerExitOperation(boolean isProcessingAllowed, AbstractOperation operation) { + try { + if (operation == null || !isProcessingAllowed || !NewRelicSecurity.isHookProcessingActive() || + NewRelicSecurity.getAgent().getSecurityMetaData().getRequest().isEmpty()) { + return; + } + NewRelicSecurity.getAgent().registerExitEvent(operation); + } catch (Throwable ignored){} + } + + public static void releaseLock(int hashcode) { + try { + GenericHelper.releaseLock(NR_SEC_CUSTOM_ATTRIB_NAME, hashcode); + } catch (Throwable ignored) {} + } + + public static boolean acquireLockIfPossible(int hashcode) { + try { + return GenericHelper.acquireLockIfPossible(NR_SEC_CUSTOM_ATTRIB_NAME, hashcode); + } catch (Throwable ignored) {} + return false; + } +} diff --git a/instrumentation-security/jcache-1.0.0/src/main/java/javax/cache/Cache_Instrumentation.java b/instrumentation-security/jcache-1.0.0/src/main/java/javax/cache/Cache_Instrumentation.java new file mode 100644 index 000000000..76f6a94f5 --- /dev/null +++ b/instrumentation-security/jcache-1.0.0/src/main/java/javax/cache/Cache_Instrumentation.java @@ -0,0 +1,286 @@ +package javax.cache; + +import com.newrelic.agent.security.instrumentation.jcache_1_0_0.JCacheHelper; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; + +import javax.cache.integration.CompletionListener; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Weave(type = MatchType.Interface, originalName = "javax.cache.Cache") +public abstract class Cache_Instrumentation { + public V get(K key) { + boolean isLockAcquired = JCacheHelper.acquireLockIfPossible(this.hashCode()); + AbstractOperation operation = null; + if (isLockAcquired) { + operation = JCacheHelper.preprocessSecurityHook(JCacheHelper.READ, Collections.singletonList(key), this.getClass().getName(), "get"); + } + V returnValue = null; + try { + returnValue = Weaver.callOriginal(); + } finally { + if(isLockAcquired) { + JCacheHelper.releaseLock(this.hashCode()); + } + } + JCacheHelper.registerExitOperation(isLockAcquired, operation); + return returnValue; + } + + public Map getAll(Set keys) { + boolean isLockAcquired = JCacheHelper.acquireLockIfPossible(this.hashCode()); + AbstractOperation operation = null; + if (isLockAcquired) { + operation = JCacheHelper.preprocessSecurityHook(JCacheHelper.READ, new ArrayList() { { addAll(keys); } }, this.getClass().getName(), "getAll"); + } + Map returnValue = null; + try { + returnValue = Weaver.callOriginal(); + } finally { + if(isLockAcquired) { + JCacheHelper.releaseLock(this.hashCode()); + } + } + JCacheHelper.registerExitOperation(isLockAcquired, operation); + return returnValue; + } + + public boolean containsKey(K key) { + boolean isLockAcquired = JCacheHelper.acquireLockIfPossible(this.hashCode()); + AbstractOperation operation = null; + if (isLockAcquired) { + operation = JCacheHelper.preprocessSecurityHook(JCacheHelper.READ, Collections.singletonList(key), this.getClass().getName(), "containsKey"); + } + boolean returnValue; + try { + returnValue = Weaver.callOriginal(); + } finally { + if(isLockAcquired) { + JCacheHelper.releaseLock(this.hashCode()); + } + } + JCacheHelper.registerExitOperation(isLockAcquired, operation); + return returnValue; + } + + public void loadAll(Set keys, boolean replaceExistingValues, CompletionListener completionListener) { + boolean isLockAcquired = JCacheHelper.acquireLockIfPossible(this.hashCode()); + AbstractOperation operation = null; + if (isLockAcquired) { + operation = JCacheHelper.preprocessSecurityHook(JCacheHelper.READ, new ArrayList() { { addAll(keys); } }, this.getClass().getName(), "loadAll"); + } + try { + Weaver.callOriginal(); + } finally { + if(isLockAcquired) { + JCacheHelper.releaseLock(this.hashCode()); + } + } + JCacheHelper.registerExitOperation(isLockAcquired, operation); + } + + public void put(K key, V value) { + boolean isLockAcquired = JCacheHelper.acquireLockIfPossible(this.hashCode()); + AbstractOperation operation = null; + if (isLockAcquired) { + operation = JCacheHelper.preprocessSecurityHook(JCacheHelper.WRITE, Arrays.asList(key, value), this.getClass().getName(), "put"); + } + try { + Weaver.callOriginal(); + } finally { + if(isLockAcquired) { + JCacheHelper.releaseLock(this.hashCode()); + } + } + JCacheHelper.registerExitOperation(isLockAcquired, operation); + } + + public V getAndPut(K key, V value) { + boolean isLockAcquired = JCacheHelper.acquireLockIfPossible(this.hashCode()); + AbstractOperation operation = null; + if (isLockAcquired) { + operation = JCacheHelper.preprocessSecurityHook(JCacheHelper.WRITE, Arrays.asList(key, value), this.getClass().getName(), "getAndPut"); + } + V returnValue; + try { + returnValue = Weaver.callOriginal(); + } finally { + if(isLockAcquired) { + JCacheHelper.releaseLock(this.hashCode()); + } + } + JCacheHelper.registerExitOperation(isLockAcquired, operation); + return returnValue; + } + + public void putAll(Map map) { + boolean isLockAcquired = JCacheHelper.acquireLockIfPossible(this.hashCode()); + AbstractOperation operation = null; + if (isLockAcquired) { + List argList = new ArrayList<>(); + for (Map.Entry entry : map.entrySet()) { + argList.add(entry.getKey()); + argList.add(entry.getValue()); + } + // do not call register exit operation method, this will lead to a verify error + // Type 'java/lang/Object' (current frame, stack[0]) is not assignable to 'com/newrelic/api/agent/security/schema/AbstractOperation' + operation = JCacheHelper.preprocessSecurityHook(JCacheHelper.WRITE, argList, this.getClass().getName(), "putAll"); + } + try { + Weaver.callOriginal(); + } finally { + if(isLockAcquired) { + JCacheHelper.releaseLock(this.hashCode()); + } + } + } + + public boolean putIfAbsent(K key, V value) { + boolean isLockAcquired = JCacheHelper.acquireLockIfPossible(this.hashCode()); + AbstractOperation operation = null; + if (isLockAcquired) { + operation = JCacheHelper.preprocessSecurityHook(JCacheHelper.WRITE, Arrays.asList(key, value), this.getClass().getName(), "putIfAbsent"); + } + boolean returnValue; + try { + returnValue = Weaver.callOriginal(); + } finally { + if(isLockAcquired) { + JCacheHelper.releaseLock(this.hashCode()); + } + } + JCacheHelper.registerExitOperation(isLockAcquired, operation); + return returnValue; + } + + public boolean remove(K key) { + boolean isLockAcquired = JCacheHelper.acquireLockIfPossible(this.hashCode()); + AbstractOperation operation = null; + if (isLockAcquired) { + operation = JCacheHelper.preprocessSecurityHook(JCacheHelper.DELETE, Collections.singletonList(key), this.getClass().getName(), "remove"); + } + boolean returnValue; + try { + returnValue = Weaver.callOriginal(); + } finally { + if(isLockAcquired) { + JCacheHelper.releaseLock(this.hashCode()); + } + } + JCacheHelper.registerExitOperation(isLockAcquired, operation); + return returnValue; + } + + public boolean remove(K key, V oldValue) { + boolean isLockAcquired = JCacheHelper.acquireLockIfPossible(this.hashCode()); + AbstractOperation operation = null; + if (isLockAcquired) { + operation = JCacheHelper.preprocessSecurityHook(JCacheHelper.DELETE, Arrays.asList(key, oldValue), this.getClass().getName(), "remove"); + } + boolean returnValue; + try { + returnValue = Weaver.callOriginal(); + } finally { + if(isLockAcquired) { + JCacheHelper.releaseLock(this.hashCode()); + } + } + JCacheHelper.registerExitOperation(isLockAcquired, operation); + return returnValue; + } + + public V getAndRemove(K key) { + boolean isLockAcquired = JCacheHelper.acquireLockIfPossible(this.hashCode()); + AbstractOperation operation = null; + if (isLockAcquired) { + operation = JCacheHelper.preprocessSecurityHook(JCacheHelper.DELETE, Collections.singletonList(key), this.getClass().getName(), "getAndRemove"); + } + V returnValue; + try { + returnValue = Weaver.callOriginal(); + } finally { + if(isLockAcquired) { + JCacheHelper.releaseLock(this.hashCode()); + } + } + JCacheHelper.registerExitOperation(isLockAcquired, operation); + return returnValue; + } + + public boolean replace(K key, V oldValue) { + boolean isLockAcquired = JCacheHelper.acquireLockIfPossible(this.hashCode()); + AbstractOperation operation = null; + if (isLockAcquired) { + operation = JCacheHelper.preprocessSecurityHook(JCacheHelper.UPDATE, Arrays.asList(key, oldValue), this.getClass().getName(), "replace"); + } + boolean returnValue; + try { + returnValue = Weaver.callOriginal(); + } finally { + if(isLockAcquired) { + JCacheHelper.releaseLock(this.hashCode()); + } + } + JCacheHelper.registerExitOperation(isLockAcquired, operation); + return returnValue; + } + + public boolean replace(K key, V oldValue, V newValue) { + boolean isLockAcquired = JCacheHelper.acquireLockIfPossible(this.hashCode()); + AbstractOperation operation = null; + if (isLockAcquired) { + operation = JCacheHelper.preprocessSecurityHook(JCacheHelper.UPDATE, Arrays.asList(key, oldValue, newValue), this.getClass().getName(), "replace"); + } + boolean returnValue; + try { + returnValue = Weaver.callOriginal(); + } finally { + if(isLockAcquired) { + JCacheHelper.releaseLock(this.hashCode()); + } + } + JCacheHelper.registerExitOperation(isLockAcquired, operation); + return returnValue; + } + + public V getAndReplace(K key, V value) { + boolean isLockAcquired = JCacheHelper.acquireLockIfPossible(this.hashCode()); + AbstractOperation operation = null; + if (isLockAcquired) { + operation = JCacheHelper.preprocessSecurityHook(JCacheHelper.UPDATE, Arrays.asList(key, value), this.getClass().getName(), "getAndReplace"); + } + V returnValue; + try { + returnValue = Weaver.callOriginal(); + } finally { + if(isLockAcquired) { + JCacheHelper.releaseLock(this.hashCode()); + } + } + JCacheHelper.registerExitOperation(isLockAcquired, operation); + return returnValue; + } + + public void removeAll(Set keys) { + boolean isLockAcquired = JCacheHelper.acquireLockIfPossible(this.hashCode()); + AbstractOperation operation = null; + if (isLockAcquired) { + operation = JCacheHelper.preprocessSecurityHook(JCacheHelper.DELETE, new ArrayList() { { addAll(keys); } }, this.getClass().getName(), "removeAll"); + } + try { + Weaver.callOriginal(); + } finally { + if(isLockAcquired) { + JCacheHelper.releaseLock(this.hashCode()); + } + } + JCacheHelper.registerExitOperation(isLockAcquired, operation); + } +} diff --git a/instrumentation-security/jcache-1.0.0/src/test/java/com/nr/agent/security/instrumentation/jcache/CustomObject.java b/instrumentation-security/jcache-1.0.0/src/test/java/com/nr/agent/security/instrumentation/jcache/CustomObject.java new file mode 100644 index 000000000..6c9bf1dcc --- /dev/null +++ b/instrumentation-security/jcache-1.0.0/src/test/java/com/nr/agent/security/instrumentation/jcache/CustomObject.java @@ -0,0 +1,33 @@ +package com.nr.agent.security.instrumentation.jcache; + +import com.hazelcast.nio.ObjectDataInput; +import com.hazelcast.nio.ObjectDataOutput; +import com.hazelcast.nio.serialization.DataSerializable; + +import java.io.IOException; + +public class CustomObject implements DataSerializable { + String name; + int age; + + public CustomObject() { + + } + + public CustomObject(String name, int age) { + this.name = name; + this.age = age; + } + + @Override + public void writeData(ObjectDataOutput out) throws IOException { + out.writeUTF(name); + out.writeInt(age); + } + + @Override + public void readData(ObjectDataInput in) throws IOException { + name = in.readUTF(); + age = in.readInt(); + } +} \ No newline at end of file diff --git a/instrumentation-security/jcache-1.0.0/src/test/java/com/nr/agent/security/instrumentation/jcache/JCacheTest.java b/instrumentation-security/jcache-1.0.0/src/test/java/com/nr/agent/security/instrumentation/jcache/JCacheTest.java new file mode 100644 index 000000000..6c654e899 --- /dev/null +++ b/instrumentation-security/jcache-1.0.0/src/test/java/com/nr/agent/security/instrumentation/jcache/JCacheTest.java @@ -0,0 +1,327 @@ +package com.nr.agent.security.instrumentation.jcache; + +import com.hazelcast.cache.HazelcastCachingProvider; +import com.hazelcast.config.Config; +import com.hazelcast.config.InMemoryFormat; +import com.hazelcast.config.MapConfig; +import com.hazelcast.core.Hazelcast; +import com.hazelcast.core.HazelcastInstance; +import com.newrelic.agent.security.introspec.InstrumentationTestConfig; +import com.newrelic.agent.security.introspec.SecurityInstrumentationTestRunner; +import com.newrelic.agent.security.introspec.SecurityIntrospector; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; +import com.newrelic.api.agent.security.schema.operation.JCacheOperation; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; + +import javax.cache.Cache; +import javax.cache.CacheManager; +import javax.cache.Caching; +import javax.cache.configuration.CompleteConfiguration; +import javax.cache.configuration.MutableConfiguration; +import javax.cache.integration.CompletionListener; +import javax.cache.spi.CachingProvider; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.UUID; + +@RunWith(SecurityInstrumentationTestRunner.class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@InstrumentationTestConfig(includePrefixes = { "javax.cache" }) +public class JCacheTest { + private static final HazelcastInstance HAZELCAST_INSTANCE = Hazelcast.newHazelcastInstance(new Config() + .addMapConfig(new MapConfig("default") + .setInMemoryFormat(InMemoryFormat.NATIVE))); + private static CacheManager cacheManager; + private static Cache cache; + + @AfterClass + public static void tearDown() { + if (cacheManager!=null && !cacheManager.isClosed()) { + cacheManager.destroyCache("test"); + cacheManager.close(); + } + } + + @Test + public void testPutGet() { + String uuid = UUID.randomUUID().toString(); + String key = "key-"+uuid; + String val = "value-"+uuid; + cache.put(key, val); + cache.get(key); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + + List operations = introspector.getOperations(); + + Assert.assertTrue("No operations detected", operations.size() > 0); + Assert.assertEquals("Incorrect number operations detected", 2, operations.size()); + + verifier((JCacheOperation) operations.get(0), Arrays.asList(key, val), "put", "write"); + verifier((JCacheOperation) operations.get(1), Collections.singletonList(key), "get", "read"); + } + + @Test + public void testPutGetObject() { + String uuid = UUID.randomUUID().toString(); + String key = "key-"+uuid; + CustomObject val = new CustomObject(uuid, 123); + cache.put(key, val); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + + List operations = introspector.getOperations(); + + Assert.assertTrue("No operations detected", operations.size() > 0); + Assert.assertEquals("Incorrect number operations detected", 1, operations.size()); + + verifier((JCacheOperation) operations.get(0), Arrays.asList(key, val), "put", "write"); + } + + @Test + public void testPutAllGetAll() { + String uuid = UUID.randomUUID().toString(); + String key = "key-"+uuid; + String val = "value-"+uuid; + cache.putAll(Collections.singletonMap(key, val)); + cache.getAll(Collections.singleton(key)); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + + List operations = introspector.getOperations(); + + Assert.assertTrue("No operations detected", operations.size() > 0); + Assert.assertEquals("Incorrect number operations detected", 2, operations.size()); + + verifier((JCacheOperation) operations.get(0), Arrays.asList(key, val), "putAll", "write"); + verifier((JCacheOperation) operations.get(1), Collections.singletonList(key), "getAll", "read"); + } + + @Test + public void testContainsKey() { + String uuid = UUID.randomUUID().toString(); + String key = "key-"+uuid; + cache.containsKey(key); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + + List operations = introspector.getOperations(); + + Assert.assertTrue("No operations detected", operations.size() > 0); + Assert.assertEquals("Incorrect number operations detected", 1, operations.size()); + + verifier((JCacheOperation) operations.get(0), Collections.singletonList(key), "containsKey", "read"); + } + + @Test + public void testLoadAll() { + String key = "key-"; + String val = "value-"; + cache.loadAll(new HashSet(){{ add(key); add(val); }}, true, new CompletionListener() { + @Override + public void onCompletion() { + + } + + @Override + public void onException(Exception e) { + + } + }); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + + List operations = introspector.getOperations(); + + Assert.assertTrue("No operations detected", operations.size() > 0); + Assert.assertEquals("Incorrect number operations detected", 1, operations.size()); + + verifier((JCacheOperation) operations.get(0), Arrays.asList(val, key), "loadAll", "read"); + } + + @Test + public void testGetAndPut() { + String uuid = UUID.randomUUID().toString(); + String key = "key-"+uuid; + String val = "value-"+uuid; + cache.getAndPut(key, val); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + + List operations = introspector.getOperations(); + + Assert.assertTrue("No operations detected", operations.size() > 0); + Assert.assertEquals("Incorrect number operations detected", 1, operations.size()); + + verifier((JCacheOperation) operations.get(0), Arrays.asList(key, val), "getAndPut", "write"); + } + + @Test + public void testPutIfAbsent() { + String uuid = UUID.randomUUID().toString(); + String key = "key-"+uuid; + String val = "value-"+uuid; + cache.putIfAbsent(key, val); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + + List operations = introspector.getOperations(); + + Assert.assertTrue("No operations detected", operations.size() > 0); + Assert.assertEquals("Incorrect number operations detected", 1, operations.size()); + + verifier((JCacheOperation) operations.get(0), Arrays.asList(key, val), "putIfAbsent", "write"); + } + + @Test + public void testRemove() { + String uuid = UUID.randomUUID().toString(); + String key = "key-"+uuid; + cache.remove(key); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + + List operations = introspector.getOperations(); + + Assert.assertTrue("No operations detected", operations.size() > 0); + Assert.assertEquals("Incorrect number operations detected", 1, operations.size()); + + verifier((JCacheOperation) operations.get(0), Collections.singletonList(key), "remove", "delete"); + } + + @Test + public void testRemove2() { + String uuid = UUID.randomUUID().toString(); + String key = "key-"+uuid; + String val = "value-"+uuid; + cache.remove(key, val); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + + List operations = introspector.getOperations(); + + Assert.assertTrue("No operations detected", operations.size() > 0); + Assert.assertEquals("Incorrect number operations detected", 1, operations.size()); + + verifier((JCacheOperation) operations.get(0), Arrays.asList(key, val), "remove", "delete"); + } + + @Test + public void testGetAndRemove() { + String uuid = UUID.randomUUID().toString(); + String key = "key-"+uuid; + cache.getAndRemove(key); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + + List operations = introspector.getOperations(); + + Assert.assertTrue("No operations detected", operations.size() > 0); + Assert.assertEquals("Incorrect number operations detected", 1, operations.size()); + + verifier((JCacheOperation) operations.get(0), Collections.singletonList(key), "getAndRemove", "delete"); + } + + @Test + public void testReplace() { + String uuid = UUID.randomUUID().toString(); + String key = "key-"+uuid; + String val = "value-"+uuid; + cache.replace(key, val); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + + List operations = introspector.getOperations(); + + Assert.assertTrue("No operations detected", operations.size() > 0); + Assert.assertEquals("Incorrect number operations detected", 1, operations.size()); + + verifier((JCacheOperation) operations.get(0), Arrays.asList(key, val), "replace", "update"); + } + + @Test + public void testReplace2() { + String uuid = UUID.randomUUID().toString(); + String key = "key-"+uuid; + String val = "value-"+uuid; + cache.replace(key, val, "test"); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + + List operations = introspector.getOperations(); + + Assert.assertTrue("No operations detected", operations.size() > 0); + Assert.assertEquals("Incorrect number operations detected", 1, operations.size()); + + verifier((JCacheOperation) operations.get(0), Arrays.asList(key, val, "test"), "replace", "update"); + } + + @Test + public void testGetAndReplace() { + String uuid = UUID.randomUUID().toString(); + String key = "key-"+uuid; + String val = "value-"+uuid; + cache.getAndReplace(key, val); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + + List operations = introspector.getOperations(); + + Assert.assertTrue("No operations detected", operations.size() > 0); + Assert.assertEquals("Incorrect number operations detected", 1, operations.size()); + + verifier((JCacheOperation) operations.get(0), Arrays.asList(key, val), "getAndReplace", "update"); + } + + @Test + public void testRemoveAll() { + String uuid = UUID.randomUUID().toString(); + String key = "key-"+uuid; + cache.removeAll(Collections.singleton(key)); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + + List operations = introspector.getOperations(); + + Assert.assertTrue("No operations detected", operations.size() > 0); + Assert.assertEquals("Incorrect number operations detected", 1, operations.size()); + + verifier((JCacheOperation) operations.get(0), Collections.singletonList(key), "removeAll", "delete"); + } + + @Test + public void testRemoveAll2() { + cache.removeAll(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + + List operations = introspector.getOperations(); + + Assert.assertEquals("Unexpected operations detected", 0, operations.size()); + } + + private static void verifier(JCacheOperation operation, List args, String method, String type) { + Assert.assertEquals("Incorrect case type.", VulnerabilityCaseType.CACHING_DATA_STORE, operation.getCaseType()); + Assert.assertEquals("Incorrect event category.", JCacheOperation.JCACHE, operation.getCategory()); + Assert.assertEquals("Incorrect command type.", type, operation.getType()); + Assert.assertEquals("Incorrect executed method name.", method, operation.getMethodName()); + Assert.assertEquals("Incorrect parameters", args, operation.getArguments()); + } + + @BeforeClass + public static void getManagerCache() { + CachingProvider provider = Caching.getCachingProvider("com.hazelcast.cache.HazelcastMemberCachingProvider"); + cacheManager = provider.getCacheManager(null, null, HazelcastCachingProvider.propertiesByInstanceItself(HAZELCAST_INSTANCE)); + CompleteConfiguration config = new MutableConfiguration().setTypes( String.class, Object.class ); + cache = cacheManager.createCache("test", config); + } +} diff --git a/instrumentation-security/jdbc-generic/src/main/java/java/sql/PreparedStatement_Instrumentation.java b/instrumentation-security/jdbc-generic/src/main/java/java/sql/PreparedStatement_Instrumentation.java index 475140ddf..fd9ebbb68 100644 --- a/instrumentation-security/jdbc-generic/src/main/java/java/sql/PreparedStatement_Instrumentation.java +++ b/instrumentation-security/jdbc-generic/src/main/java/java/sql/PreparedStatement_Instrumentation.java @@ -8,6 +8,7 @@ package java.sql; import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; import com.newrelic.api.agent.security.instrumentation.helpers.JdbcHelper; import com.newrelic.api.agent.security.schema.AbstractOperation; import com.newrelic.api.agent.security.schema.JDBCVendor; @@ -22,6 +23,7 @@ import java.math.BigDecimal; import java.util.HashMap; import java.util.Map; +import java.util.regex.Matcher; @Weave(originalName = "java.sql.PreparedStatement", type = MatchType.Interface) public abstract class PreparedStatement_Instrumentation { @@ -53,6 +55,21 @@ private AbstractOperation preprocessSecurityHook (String sql, String methodName) SQLOperation sqlOperation = new SQLOperation(this.getClass().getName(), methodName); sqlOperation.setQuery(sql); sqlOperation.setParams(this.params); + + // first check for quoted strings and remove them for final check + String localSqlCopy = new String(sql); + Matcher quotedStringMatcher = GenericHelper.QUOTED_STRING_PATTERN.matcher(localSqlCopy); + while (quotedStringMatcher.find()) { + String replaceChars = quotedStringMatcher.group(); + localSqlCopy = localSqlCopy.replace(replaceChars, "_TEMP_"); + } + // final check to identify the stored procedure call + Matcher storedProcedureMatcher = GenericHelper.STORED_PROCEDURE_PATTERN.matcher(localSqlCopy); + while (storedProcedureMatcher.find()) { + sqlOperation.setStoredProcedureCall(true); + break; + } + sqlOperation.setDbName(NewRelicSecurity.getAgent().getSecurityMetaData().getCustomAttribute(JDBCVendor.META_CONST_JDBC_VENDOR, String.class)); sqlOperation.setPreparedCall(true); NewRelicSecurity.getAgent().registerOperation(sqlOperation); diff --git a/instrumentation-security/jdbc-generic/src/main/java/java/sql/Statement_Instrumentation.java b/instrumentation-security/jdbc-generic/src/main/java/java/sql/Statement_Instrumentation.java index a12027f0a..13adade45 100644 --- a/instrumentation-security/jdbc-generic/src/main/java/java/sql/Statement_Instrumentation.java +++ b/instrumentation-security/jdbc-generic/src/main/java/java/sql/Statement_Instrumentation.java @@ -8,6 +8,7 @@ package java.sql; import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; import com.newrelic.api.agent.security.instrumentation.helpers.JdbcHelper; import com.newrelic.api.agent.security.schema.AbstractOperation; import com.newrelic.api.agent.security.schema.JDBCVendor; @@ -18,6 +19,7 @@ import com.newrelic.api.agent.weaver.NewField; import com.newrelic.api.agent.weaver.Weave; import com.newrelic.api.agent.weaver.Weaver; +import java.util.regex.Matcher; @Weave(originalName = "java.sql.Statement", type = MatchType.Interface) public abstract class Statement_Instrumentation { @@ -45,6 +47,21 @@ private AbstractOperation preprocessSecurityHook (String sql, String methodName) } SQLOperation sqlOperation = new SQLOperation(this.getClass().getName(), methodName); sqlOperation.setQuery(sql); + + // first check for quoted strings and remove them for final check + String localSqlCopy = new String(sql); + Matcher quotedStringMatcher = GenericHelper.QUOTED_STRING_PATTERN.matcher(localSqlCopy); + while (quotedStringMatcher.find()) { + String replaceChars = quotedStringMatcher.group(); + localSqlCopy = localSqlCopy.replace(replaceChars, "_TEMP_"); + } + // final check to identify the stored procedure call + Matcher storedProcedureMatcher = GenericHelper.STORED_PROCEDURE_PATTERN.matcher(localSqlCopy); + while (storedProcedureMatcher.find()) { + sqlOperation.setStoredProcedureCall(true); + break; + } + sqlOperation.setDbName(NewRelicSecurity.getAgent().getSecurityMetaData().getCustomAttribute(JDBCVendor.META_CONST_JDBC_VENDOR, String.class)); sqlOperation.setPreparedCall(false); NewRelicSecurity.getAgent().registerOperation(sqlOperation); diff --git a/instrumentation-security/jdbc-generic/src/test/java/com/nr/instrumentation/java/sql/StoredProcedureTest.java b/instrumentation-security/jdbc-generic/src/test/java/com/nr/instrumentation/java/sql/StoredProcedureTest.java new file mode 100644 index 000000000..ddf26ccba --- /dev/null +++ b/instrumentation-security/jdbc-generic/src/test/java/com/nr/instrumentation/java/sql/StoredProcedureTest.java @@ -0,0 +1,187 @@ +package com.nr.instrumentation.java.sql; + +import com.newrelic.agent.security.introspec.InstrumentationTestConfig; +import com.newrelic.agent.security.introspec.SecurityInstrumentationTestRunner; +import com.newrelic.agent.security.introspec.SecurityIntrospector; +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; +import com.newrelic.api.agent.security.schema.operation.BatchSQLOperation; +import com.newrelic.api.agent.security.schema.operation.SQLOperation; +import org.h2.jdbc.JdbcPreparedStatement; +import org.h2.jdbc.JdbcStatement; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; + +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.Date; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +@RunWith(SecurityInstrumentationTestRunner.class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@InstrumentationTestConfig(includePrefixes = { "javax.sql", "java.sql" }) +public class StoredProcedureTest { + private static final String DB_DRIVER = "org.h2.Driver"; + private static final String DB_CONNECTION = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"; + private static final String DB_USER = ""; + private static final String DB_PASSWORD = ""; + private static final Connection CONNECTION = getDBConnection(); + + private static final List QUERIES = new ArrayList<>(); + private static final List FAIL_QUERIES = new ArrayList<>(); + + @AfterClass + public static void teardown() throws SQLException { + CONNECTION.close(); + } + + private static Connection getDBConnection() { + Connection dbConnection = null; + try { + Class.forName(DB_DRIVER); + dbConnection = DriverManager.getConnection(DB_CONNECTION, DB_USER, DB_PASSWORD); + return dbConnection; + } catch (Exception e) { + e.printStackTrace(); + } + return dbConnection; + } + + @BeforeClass + public static void initData() throws SQLException { + QUERIES.add("CREATE ALIAS getH2Version FOR \"org.h2.engine.Constants.getVersion\""); + QUERIES.add("call getH2Version()"); + QUERIES.add(" call getH2Version() "); + QUERIES.add("{call getH2Version()}"); + QUERIES.add("{ call getH2Version() }"); + QUERIES.add(" { call getH2Version() } "); + QUERIES.add(" { call getH2Version() } "); + + QUERIES.add("CALL getH2Version()"); + QUERIES.add(" CALL getH2Version() "); + QUERIES.add("{CALL getH2Version()}"); + QUERIES.add("{ CALL getH2Version() }"); + QUERIES.add(" { CALL getH2Version() } "); + QUERIES.add(" { CALL getH2Version() } "); + + FAIL_QUERIES.add("CREATE TABLE IF NOT EXISTS MYUSERS(id int primary key, first_name varchar(255), last_name varchar(255))"); + FAIL_QUERIES.add("{select * from myusers where first_name='call'}"); + FAIL_QUERIES.add("{select * from myusers where first_name='{call'}"); + FAIL_QUERIES.add("select * from myusers where first_name='call'"); + FAIL_QUERIES.add("select * from myusers where first_name='{call'"); + FAIL_QUERIES.add("SELECT * FROM MYUSERS WHERE first_name='call'"); + FAIL_QUERIES.add("SELECT * FROM MYUSERS WHERE first_name='{call'"); + FAIL_QUERIES.add("{SELECT * FROM MYUSERS WHERE first_name='call'}"); + FAIL_QUERIES.add("{SELECT * FROM MYUSERS WHERE first_name='{call'}"); + FAIL_QUERIES.add("{select * from myusers where first_name='{call func()}'}"); + + // set up data in h2 + Statement stmt = CONNECTION.createStatement(); + stmt.execute(QUERIES.get(0)); + stmt.execute(FAIL_QUERIES.get(0)); + stmt.close(); + } + + @Test + public void testProcedureCasePass() throws SQLException { + for (int i = 1; i < QUERIES.size(); i++) { + _case1(QUERIES.get(i)); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(i-1); + + Assert.assertEquals(String.format("[case-%d] Invalid executed parameters.", i), QUERIES.get(i), operation.getQuery()); + Assert.assertEquals(String.format("[case-%d] Invalid event category.", i), VulnerabilityCaseType.SQL_DB_COMMAND, operation.getCaseType()); + Assert.assertTrue(String.format("[case-%d] Expected a stored procedure call.", i), operation.isStoredProcedureCall()); + Assert.assertEquals(String.format("[case-%d] Invalid executed class name.", i), JdbcStatement.class.getName(), operation.getClassName()); + Assert.assertEquals(String.format("[case-%d] Invalid executed method name.", i), "execute", operation.getMethodName()); + } + } + + @Test + public void testProcedureCaseFail() throws SQLException { + for (int i = 1; i < FAIL_QUERIES.size(); i++) { + _case1(FAIL_QUERIES.get(i)); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(i-1); + + Assert.assertEquals(String.format("[case-%d] Invalid executed parameters.", i), FAIL_QUERIES.get(i), operation.getQuery()); + Assert.assertEquals(String.format("[case-%d] Invalid event category.", i), VulnerabilityCaseType.SQL_DB_COMMAND, operation.getCaseType()); + Assert.assertFalse(String.format("[case-%d] Expected a stored procedure call.", i), operation.isStoredProcedureCall()); + Assert.assertEquals(String.format("[case-%d] Invalid executed class name.", i), JdbcStatement.class.getName(), operation.getClassName()); + Assert.assertEquals(String.format("[case-%d] Invalid executed method name.", i), "execute", operation.getMethodName()); + } + } + + @Test + public void testPreparedProcedureCasePass() throws SQLException { + for (int i = 1; i < QUERIES.size(); i++) { + _case2(QUERIES.get(i)); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(i-1); + + Assert.assertEquals(String.format("[case-%d] Invalid executed parameters.", i), QUERIES.get(i), operation.getQuery()); + Assert.assertEquals(String.format("[case-%d] Invalid event category.", i), VulnerabilityCaseType.SQL_DB_COMMAND, operation.getCaseType()); + Assert.assertTrue(String.format("[case-%d] Expected a stored procedure call.", i), operation.isStoredProcedureCall()); + Assert.assertEquals(String.format("[case-%d] Invalid executed class name.", i), JdbcPreparedStatement.class.getName(), operation.getClassName()); + Assert.assertEquals(String.format("[case-%d] Invalid executed method name.", i), "execute", operation.getMethodName()); + } + } + + @Test + public void testPreparedProcedureCaseFail() throws SQLException { + for (int i = 1; i < FAIL_QUERIES.size(); i++) { + _case2(FAIL_QUERIES.get(i)); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + SQLOperation operation = (SQLOperation) operations.get(i-1); + + Assert.assertEquals(String.format("[case-%d] Invalid executed parameters.", i), FAIL_QUERIES.get(i), operation.getQuery()); + Assert.assertEquals(String.format("[case-%d] Invalid event category.", i), VulnerabilityCaseType.SQL_DB_COMMAND, operation.getCaseType()); + Assert.assertFalse(String.format("[case-%d] Expected a stored procedure call.", i), operation.isStoredProcedureCall()); + Assert.assertEquals(String.format("[case-%d] Invalid executed class name.", i), JdbcPreparedStatement.class.getName(), operation.getClassName()); + Assert.assertEquals(String.format("[case-%d] Invalid executed method name.", i), "execute", operation.getMethodName()); + } + } + + @Trace(dispatcher = true) + private void _case1(String sql) throws SQLException { + Statement stmt = CONNECTION.createStatement(); + stmt.execute(sql); + stmt.close(); + } + + @Trace(dispatcher = true) + private void _case2(String sql) throws SQLException { + PreparedStatement stmt = CONNECTION.prepareStatement(sql); + stmt.execute(); + stmt.close(); + } +} diff --git a/instrumentation-security/jersey-2.16/build.gradle b/instrumentation-security/jersey-2.16/build.gradle new file mode 100644 index 000000000..d10b64445 --- /dev/null +++ b/instrumentation-security/jersey-2.16/build.gradle @@ -0,0 +1,24 @@ +dependencies { + implementation(project(":newrelic-security-api")) + implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}") + implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}") + implementation("org.glassfish.jersey.core:jersey-server:2.16") + +} + +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.jersey-2-16' } +} + +// org.glassfish.jersey.core 2.28 version starts pulling in jakarata jar named dependencies. +// Version 3.0.0-M1 starts pulling in jakarata with renamed jar and packages +verifyInstrumentation { + passesOnly 'org.glassfish.jersey.core:jersey-server:[2.16,3.0)' + exclude 'org.glassfish.jersey.core:jersey-server:[2.0-m05-2,2.0)' + excludeRegex '.*-(M|RC)[0-9]*' +} + +site { + title 'Jersey' + type 'Framework' +} \ No newline at end of file diff --git a/instrumentation-security/jersey-2.16/src/main/java/com/newrelic/agent/security/instrumentation/jersey2/ApplicationHandler_Handler.java b/instrumentation-security/jersey-2.16/src/main/java/com/newrelic/agent/security/instrumentation/jersey2/ApplicationHandler_Handler.java new file mode 100644 index 000000000..09844d08b --- /dev/null +++ b/instrumentation-security/jersey-2.16/src/main/java/com/newrelic/agent/security/instrumentation/jersey2/ApplicationHandler_Handler.java @@ -0,0 +1,39 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.security.instrumentation.jersey2; + +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import org.glassfish.jersey.server.ContainerRequest; + +import java.io.OutputStream; + +@Weave(type = MatchType.ExactClass, originalName = "org.glassfish.jersey.server.ApplicationHandler") +public abstract class ApplicationHandler_Handler { + + public void handle(ContainerRequest requestContext) { + boolean isRequestLockAcquired = false; + try { + if (requestContext != null) { + isRequestLockAcquired = HttpRequestHelper.acquireRequestLockIfPossible(); + if (isRequestLockAcquired) { + HttpRequestHelper.preprocessSecurityHook(requestContext); + HttpRequestHelper.registerUserLevelCode("JERSEY"); + } + } + Weaver.callOriginal(); + } finally { + if(isRequestLockAcquired){ + HttpRequestHelper.releaseRequestLock(); + } + } + } + +} diff --git a/instrumentation-security/jersey-2.16/src/main/java/com/newrelic/agent/security/instrumentation/jersey2/ContainerResponse_Instrumentation.java b/instrumentation-security/jersey-2.16/src/main/java/com/newrelic/agent/security/instrumentation/jersey2/ContainerResponse_Instrumentation.java new file mode 100644 index 000000000..aa015732b --- /dev/null +++ b/instrumentation-security/jersey-2.16/src/main/java/com/newrelic/agent/security/instrumentation/jersey2/ContainerResponse_Instrumentation.java @@ -0,0 +1,53 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.security.instrumentation.jersey2; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.security.schema.StringUtils; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weaver; +import org.glassfish.jersey.message.internal.OutboundJaxrsResponse; + +import com.newrelic.api.agent.weaver.Weave; +import org.glassfish.jersey.message.internal.OutboundMessageContext; +import org.glassfish.jersey.server.ContainerRequest; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper.SERVLET_GET_IS_OPERATION_LOCK; + +@Weave(type = MatchType.ExactClass, originalName = "org.glassfish.jersey.server.ContainerResponse") +public abstract class ContainerResponse_Instrumentation { + + ContainerResponse_Instrumentation(final ContainerRequest requestContext, final OutboundJaxrsResponse response) { + if(response != null && response.getContext() != null && response.getContext().hasEntity()){ + Object responseObject = response.getContext().getEntity(); + NewRelicSecurity.getAgent().getSecurityMetaData().getResponse().setResponseBody(new StringBuilder(String.valueOf(responseObject))); + } + } + + public abstract OutboundMessageContext getWrappedMessageContext(); + + public void close() { + boolean isLockAcquired = false; + try { + isLockAcquired = GenericHelper.acquireLockIfPossible(SERVLET_GET_IS_OPERATION_LOCK); + if(isLockAcquired) { + HttpRequestHelper.postProcessSecurityHook(this.getClass().getName(), getWrappedMessageContext()); + } + Weaver.callOriginal(); + } finally { + if(isLockAcquired) { + GenericHelper.releaseLock(SERVLET_GET_IS_OPERATION_LOCK); + } + } + } +} diff --git a/instrumentation-security/jersey-2.16/src/main/java/com/newrelic/agent/security/instrumentation/jersey2/EntityInputStream_Instrumentation.java b/instrumentation-security/jersey-2.16/src/main/java/com/newrelic/agent/security/instrumentation/jersey2/EntityInputStream_Instrumentation.java new file mode 100644 index 000000000..b82213cb4 --- /dev/null +++ b/instrumentation-security/jersey-2.16/src/main/java/com/newrelic/agent/security/instrumentation/jersey2/EntityInputStream_Instrumentation.java @@ -0,0 +1,33 @@ +package com.newrelic.agent.security.instrumentation.jersey2; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; + +import java.io.InputStream; + +import static com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper.SERVLET_GET_IS_OPERATION_LOCK; + +@Weave(type = MatchType.ExactClass, originalName = "org.glassfish.jersey.message.internal.EntityInputStream") +public class EntityInputStream_Instrumentation { + + public final InputStream getWrappedStream() { + InputStream retunObject; + boolean isLockAcquired = false; + try { + isLockAcquired = GenericHelper.acquireLockIfPossible(SERVLET_GET_IS_OPERATION_LOCK); + retunObject = Weaver.callOriginal(); + if (isLockAcquired && NewRelicSecurity.isHookProcessingActive() && retunObject != null) { + HttpRequestHelper.registerInputStreamHashIfNeeded(retunObject.hashCode()); + } + } finally { + if(isLockAcquired) { + GenericHelper.releaseLock(SERVLET_GET_IS_OPERATION_LOCK); + } + } + return retunObject; + } + +} diff --git a/instrumentation-security/jersey-2.16/src/main/java/com/newrelic/agent/security/instrumentation/jersey2/HttpRequestHelper.java b/instrumentation-security/jersey-2.16/src/main/java/com/newrelic/agent/security/instrumentation/jersey2/HttpRequestHelper.java new file mode 100644 index 000000000..3d9ffe1c1 --- /dev/null +++ b/instrumentation-security/jersey-2.16/src/main/java/com/newrelic/agent/security/instrumentation/jersey2/HttpRequestHelper.java @@ -0,0 +1,304 @@ +package com.newrelic.agent.security.instrumentation.jersey2; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.security.instrumentation.helpers.LowSeverityHelper; +import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; +import com.newrelic.api.agent.security.schema.AgentMetaData; +import com.newrelic.api.agent.security.schema.HttpRequest; +import com.newrelic.api.agent.security.schema.SecurityMetaData; +import com.newrelic.api.agent.security.schema.StringUtils; +import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityException; +import com.newrelic.api.agent.security.schema.operation.RXSSOperation; +import com.newrelic.api.agent.security.schema.policy.AgentPolicy; +import org.glassfish.jersey.internal.PropertiesDelegate; +import org.glassfish.jersey.message.internal.OutboundMessageContext; +import org.glassfish.jersey.server.ContainerRequest; + +import javax.ws.rs.core.MultivaluedMap; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; + +public class HttpRequestHelper { + + private static final String X_FORWARDED_FOR = "x-forwarded-for"; + private static final String EMPTY = ""; + public static final String QUESTION_MARK = "?"; + public static final String CONTAINER_RESPONSE_METHOD_NAME = "ContainerResponse"; + + public static final String NR_SEC_CUSTOM_ATTRIB_NAME = "REQUEST_LOCK-"; + private static final String WILDCARD = "*"; + private static final String SEPARATOR = "/"; + public static final String HEADER_SEPARATOR = ";"; + public static final String GRIZZLY_REQUEST_PROPERTIES_DELEGATE = "GRIZZLY_REQUEST_PROPERTIES_DELEGATE"; + public static final String GRIZZLY_REQUEST = "GRIZZLY_REQUEST"; + public static final String ORG_GLASSFISH_JERSEY_GRIZZLY_2_HTTPSERVER_GRIZZLY_REQUEST_PROPERTIES_DELEGATE = "org.glassfish.jersey.grizzly2.httpserver.GrizzlyRequestPropertiesDelegate"; + public static final String ORG_GLASSFISH_GRIZZLY_HTTP_SERVER_REQUEST = "org.glassfish.grizzly.http.server.Request"; + public static final String FIELD_REQUEST = "request"; + public static final String METHOD_GET_REMOTE_ADDR = "getRemoteAddr"; + public static final String METHOD_GET_REMOTE_PORT = "getRemotePort"; + public static final String METHOD_GET_LOCAL_PORT = "getLocalPort"; + public static final String METHOD_GET_SCHEME = "getScheme"; + public static final String METHOD_GET_CONTENT_TYPE = "getContentType"; + public static final String ORG_GLASSFISH_JERSEY_GRIZZLY_2_HTTPSERVER_TRACING_AWARE_PROPERTIES_DELEGATE = "org.glassfish.jersey.message.internal.TracingAwarePropertiesDelegate"; + public static final String TRACING_AWARE_PROPERTIES_DELEGATE = "TRACING_AWARE_PROPERTIES_DELEGATE"; + public static final String FIELD_PROPERTIES_DELEGATE = "propertiesDelegate"; + + private static final String REQUEST_INPUTSTREAM_HASH = "REQUEST_INPUTSTREAM_HASH"; + + public static Class grizzlyRequestPropertiesDelegateKlass = null; + + public static Class grizzlyRequest = null; + + public static Class tracingAwarePropertiesDelegateKlass = null; + + public static void preprocessSecurityHook(ContainerRequest requestContext) { + try { + if (!NewRelicSecurity.isHookProcessingActive()) { + return; + } + SecurityMetaData securityMetaData = NewRelicSecurity.getAgent().getSecurityMetaData(); + + HttpRequest securityRequest = securityMetaData.getRequest(); + if (securityRequest.isRequestParsed()) { + return; + } + AgentMetaData securityAgentMetaData = securityMetaData.getMetaData(); + securityRequest.setMethod(requestContext.getMethod()); + HttpRequestHelper.processPropertiesDelegate(requestContext.getPropertiesDelegate(), securityRequest); + + if (securityRequest.getClientIP() != null && !securityRequest.getClientIP().trim().isEmpty()) { + securityAgentMetaData.getIps().add(securityRequest.getClientIP()); + } + HttpRequestHelper.processHttpRequestHeader(requestContext, securityRequest); + + securityMetaData.setTracingHeaderValue(HttpRequestHelper.getTraceHeader(securityRequest.getHeaders())); + securityRequest.setUrl(requestContext.getRequestUri().toString()); + + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + securityMetaData.getMetaData().setServiceTrace(Arrays.copyOfRange(trace, 2, trace.length)); + securityRequest.setRequestParsed(true); + } catch (Throwable ignored) { + } + } + + public static void postProcessSecurityHook(String className, OutboundMessageContext wrappedMessageContext) { + try { + if (!NewRelicSecurity.isHookProcessingActive() + ) { + return; + } + NewRelicSecurity.getAgent().getSecurityMetaData().getResponse().setHeaders(getHeaders(wrappedMessageContext)); + //Add request URI hash to low severity event filter + LowSeverityHelper.addRrequestUriToEventFilter(NewRelicSecurity.getAgent().getSecurityMetaData().getRequest()); + + RXSSOperation rxssOperation = new RXSSOperation(NewRelicSecurity.getAgent().getSecurityMetaData().getRequest(), + NewRelicSecurity.getAgent().getSecurityMetaData().getResponse(), + className, HttpRequestHelper.CONTAINER_RESPONSE_METHOD_NAME); + NewRelicSecurity.getAgent().registerOperation(rxssOperation); + ServletHelper.tmpFileCleanUp(NewRelicSecurity.getAgent().getSecurityMetaData().getFuzzRequestIdentifier().getTempFiles()); + } catch (Throwable e) { + if(e instanceof NewRelicSecurityException){ + e.printStackTrace(); + throw e; + } + } + } + + private static Map getHeaders(OutboundMessageContext outboundMessageContext) { + Map headers = new HashMap<>(); + if(outboundMessageContext == null || outboundMessageContext.getHeaders() == null){ + return headers; + } + for (String key : outboundMessageContext.getStringHeaders().keySet()) { + headers.put(key, outboundMessageContext.getHeaderString(key)); + if(StringUtils.equalsAny(StringUtils.lowerCase(key), "content-type", "contenttype")){ + NewRelicSecurity.getAgent().getSecurityMetaData().getResponse().setResponseContentType(outboundMessageContext.getHeaderString(key)); + } + } + return headers; + + } + + public static void processHttpRequestHeader(ContainerRequest request, HttpRequest securityRequest){ + MultivaluedMap headers = request.getHeaders(); + for (Map.Entry> header : headers.entrySet()) { + boolean takeNextValue = false; + String headerKey = header.getKey(); + String headerFullValue = getHeaderValue(header.getValue()); + if(headerKey != null){ + headerKey = headerKey.toLowerCase(); + } + AgentPolicy agentPolicy = NewRelicSecurity.getAgent().getCurrentPolicy(); + AgentMetaData agentMetaData = NewRelicSecurity.getAgent().getSecurityMetaData().getMetaData(); + if (agentPolicy != null + && agentPolicy.getProtectionMode().getEnabled() + && agentPolicy.getProtectionMode().getIpBlocking().getEnabled() + && agentPolicy.getProtectionMode().getIpBlocking().getIpDetectViaXFF() + && X_FORWARDED_FOR.equals(headerKey)) { + takeNextValue = true; + } else if (ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID.equals(headerKey)) { + // TODO: May think of removing this intermediate obj and directly create K2 Identifier. + NewRelicSecurity.getAgent().getSecurityMetaData() + .setFuzzRequestIdentifier(ServletHelper.parseFuzzRequestIdentifierHeader(headerFullValue)); + } else if(GenericHelper.CSEC_PARENT_ID.equals(headerKey)) { + NewRelicSecurity.getAgent().getSecurityMetaData() + .addCustomAttribute(GenericHelper.CSEC_PARENT_ID, headerFullValue); + } + + for (String headerValue : header.getValue()) { + if (headerValue != null && !headerValue.trim().isEmpty()) { + if (takeNextValue) { + agentMetaData.setClientDetectedFromXFF(true); + securityRequest.setClientIP(headerValue); + agentMetaData.getIps() + .add(securityRequest.getClientIP()); + securityRequest.setClientPort(EMPTY); + takeNextValue = false; + } + } + } + securityRequest.getHeaders().put(headerKey, headerFullValue); + } + } + + private static String getHeaderValue(List values) { + StringBuilder finalValue = new StringBuilder(); + for (String value : values) { + if (finalValue.length() > 0) { + finalValue.append(HEADER_SEPARATOR); + } + finalValue.append(value); + } + return finalValue.toString(); + } + + public static String getTraceHeader(Map headers) { + String data = EMPTY; + if (headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER) || headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())) { + data = headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER); + if (data == null || data.trim().isEmpty()) { + data = headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase()); + } + } + return data; + } + + public static boolean isRequestLockAcquired() { + try { + return NewRelicSecurity.isHookProcessingActive() && + Boolean.TRUE.equals(NewRelicSecurity.getAgent().getSecurityMetaData().getCustomAttribute(getNrSecCustomAttribName(), Boolean.class)); + } catch (Throwable ignored) {} + return false; + } + + public static boolean acquireRequestLockIfPossible() { + try { + if (NewRelicSecurity.isHookProcessingActive() && + !isRequestLockAcquired()) { + NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(getNrSecCustomAttribName(), true); + return true; + } + } catch (Throwable ignored){} + return false; + } + + public static void releaseRequestLock() { + try { + if(NewRelicSecurity.isHookProcessingActive()) { + NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(getNrSecCustomAttribName(), null); + } + } catch (Throwable ignored){} + } + + private static String getNrSecCustomAttribName() { + return NR_SEC_CUSTOM_ATTRIB_NAME + Thread.currentThread().getId(); + } + + public static void processPropertiesDelegate(PropertiesDelegate propertiesDelegate, HttpRequest securityRequest) { + if(StringUtils.equals(propertiesDelegate.getClass().getName(), ORG_GLASSFISH_JERSEY_GRIZZLY_2_HTTPSERVER_GRIZZLY_REQUEST_PROPERTIES_DELEGATE)){ + try { + Class grizzlyRequestPropertiesDelegateKlass = getClass(GRIZZLY_REQUEST_PROPERTIES_DELEGATE); + Field requestField = grizzlyRequestPropertiesDelegateKlass.getDeclaredField(FIELD_REQUEST); + requestField.setAccessible(true); + Object requestObject = requestField.get(propertiesDelegate); + Class requestClass = getClass(GRIZZLY_REQUEST); + Method getRemoteAddr = requestClass.getDeclaredMethod(METHOD_GET_REMOTE_ADDR); + Method getRemotePort = requestClass.getDeclaredMethod(METHOD_GET_REMOTE_PORT); + Method getLocalPort = requestClass.getDeclaredMethod(METHOD_GET_LOCAL_PORT); + Method getScheme = requestClass.getDeclaredMethod(METHOD_GET_SCHEME); + Method getContentType = requestClass.getDeclaredMethod(METHOD_GET_CONTENT_TYPE); + securityRequest.setClientIP(String.valueOf(getRemoteAddr.invoke(requestObject))); + securityRequest.setClientPort(String.valueOf(getRemotePort.invoke(requestObject))); + securityRequest.setServerPort((int) getLocalPort.invoke(requestObject)); + securityRequest.setProtocol((String) getScheme.invoke(requestObject)); + securityRequest.setContentType((String) getContentType.invoke(requestObject)); + } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException | NoSuchMethodException | + InvocationTargetException e) { + } + + } else if (StringUtils.equals(propertiesDelegate.getClass().getName(), ORG_GLASSFISH_JERSEY_GRIZZLY_2_HTTPSERVER_TRACING_AWARE_PROPERTIES_DELEGATE)){ + try { + Class tracingAwarePropertiesDelegateKlass = getClass(TRACING_AWARE_PROPERTIES_DELEGATE); + Field propertiesDelegateField = tracingAwarePropertiesDelegateKlass.getDeclaredField(FIELD_PROPERTIES_DELEGATE); + propertiesDelegateField.setAccessible(true); + Object propertiesDelegateObject = propertiesDelegateField.get(propertiesDelegate); + processPropertiesDelegate((PropertiesDelegate) propertiesDelegateObject, securityRequest); + } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) { + } + } else { +// System.out.println(propertiesDelegate + " : " + propertiesDelegate.getClass().getName()); + } + } + + private static Class getClass(String klassName) throws ClassNotFoundException { + switch (klassName) { + case GRIZZLY_REQUEST_PROPERTIES_DELEGATE: + if (grizzlyRequestPropertiesDelegateKlass == null) { + grizzlyRequestPropertiesDelegateKlass = Class.forName(ORG_GLASSFISH_JERSEY_GRIZZLY_2_HTTPSERVER_GRIZZLY_REQUEST_PROPERTIES_DELEGATE); + } + return grizzlyRequestPropertiesDelegateKlass; + case GRIZZLY_REQUEST: + if (grizzlyRequest == null) { + grizzlyRequest = Class.forName(ORG_GLASSFISH_GRIZZLY_HTTP_SERVER_REQUEST); + } + return grizzlyRequest; + case TRACING_AWARE_PROPERTIES_DELEGATE: + if (tracingAwarePropertiesDelegateKlass == null) { + tracingAwarePropertiesDelegateKlass = Class.forName(ORG_GLASSFISH_JERSEY_GRIZZLY_2_HTTPSERVER_TRACING_AWARE_PROPERTIES_DELEGATE); + } + return tracingAwarePropertiesDelegateKlass; + default: + throw new ClassNotFoundException(klassName); + } + } + + public static void registerInputStreamHashIfNeeded(int inputStreamHash){ + try { + Set hashSet = NewRelicSecurity.getAgent().getSecurityMetaData().getCustomAttribute(REQUEST_INPUTSTREAM_HASH, Set.class); + if(hashSet == null){ + hashSet = new HashSet<>(); + NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(REQUEST_INPUTSTREAM_HASH, hashSet); + } + hashSet.add(inputStreamHash); + } catch (Throwable ignored) {} + } + + public static void registerUserLevelCode(String frameworkName) { + try { + if (!NewRelicSecurity.isHookProcessingActive() || NewRelicSecurity.getAgent().getSecurityMetaData().getRequest().isEmpty() + ) { + return; + } + SecurityMetaData securityMetaData = NewRelicSecurity.getAgent().getSecurityMetaData(); + if (!securityMetaData.getMetaData().isUserLevelServiceMethodEncountered(frameworkName)) { + securityMetaData.getMetaData().setUserLevelServiceMethodEncountered(true); + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + securityMetaData.getMetaData().setServiceTrace(Arrays.copyOfRange(trace, 2, trace.length)); + } + } catch (Throwable ignored) { + } + } +} diff --git a/instrumentation-security/jersey-2/build.gradle b/instrumentation-security/jersey-2/build.gradle new file mode 100644 index 000000000..6b8971009 --- /dev/null +++ b/instrumentation-security/jersey-2/build.gradle @@ -0,0 +1,24 @@ +dependencies { + implementation(project(":newrelic-security-api")) + implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}") + implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}") + implementation("org.glassfish.jersey.core:jersey-server:2.0") + +} + +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.jersey-2' } +} + +// org.glassfish.jersey.core 2.28 version starts pulling in jakarata jar named dependencies. +// Version 3.0.0-M1 starts pulling in jakarata with renamed jar and packages +verifyInstrumentation { + passesOnly 'org.glassfish.jersey.core:jersey-server:[2.0,2.16)' + exclude 'org.glassfish.jersey.core:jersey-server:[2.0-m05-2,2.0)' + excludeRegex '.*-(M|RC)[0-9]*' +} + +site { + title 'Jersey' + type 'Framework' +} \ No newline at end of file diff --git a/instrumentation-security/jersey-2/src/main/java/com/newrelic/agent/security/instrumentation/jersey2/ApplicationHandler_Handler.java b/instrumentation-security/jersey-2/src/main/java/com/newrelic/agent/security/instrumentation/jersey2/ApplicationHandler_Handler.java new file mode 100644 index 000000000..09844d08b --- /dev/null +++ b/instrumentation-security/jersey-2/src/main/java/com/newrelic/agent/security/instrumentation/jersey2/ApplicationHandler_Handler.java @@ -0,0 +1,39 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.security.instrumentation.jersey2; + +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import org.glassfish.jersey.server.ContainerRequest; + +import java.io.OutputStream; + +@Weave(type = MatchType.ExactClass, originalName = "org.glassfish.jersey.server.ApplicationHandler") +public abstract class ApplicationHandler_Handler { + + public void handle(ContainerRequest requestContext) { + boolean isRequestLockAcquired = false; + try { + if (requestContext != null) { + isRequestLockAcquired = HttpRequestHelper.acquireRequestLockIfPossible(); + if (isRequestLockAcquired) { + HttpRequestHelper.preprocessSecurityHook(requestContext); + HttpRequestHelper.registerUserLevelCode("JERSEY"); + } + } + Weaver.callOriginal(); + } finally { + if(isRequestLockAcquired){ + HttpRequestHelper.releaseRequestLock(); + } + } + } + +} diff --git a/instrumentation-security/jersey-2/src/main/java/com/newrelic/agent/security/instrumentation/jersey2/ContainerResponse_Instrumentation.java b/instrumentation-security/jersey-2/src/main/java/com/newrelic/agent/security/instrumentation/jersey2/ContainerResponse_Instrumentation.java new file mode 100644 index 000000000..cf33416ab --- /dev/null +++ b/instrumentation-security/jersey-2/src/main/java/com/newrelic/agent/security/instrumentation/jersey2/ContainerResponse_Instrumentation.java @@ -0,0 +1,49 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.security.instrumentation.jersey2; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.security.schema.StringUtils; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weaver; +import org.glassfish.jersey.message.internal.OutboundJaxrsResponse; + +import com.newrelic.api.agent.weaver.Weave; +import org.glassfish.jersey.message.internal.OutboundMessageContext; +import org.glassfish.jersey.server.ContainerRequest; + +import static com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper.SERVLET_GET_IS_OPERATION_LOCK; + +@Weave(type = MatchType.ExactClass, originalName = "org.glassfish.jersey.server.ContainerResponse") +public abstract class ContainerResponse_Instrumentation { + + ContainerResponse_Instrumentation(final ContainerRequest requestContext, final OutboundJaxrsResponse response) { + if(response != null && response.getContext() != null && response.getContext().hasEntity()){ + Object responseObject = response.getContext().getEntity(); + NewRelicSecurity.getAgent().getSecurityMetaData().getResponse().setResponseBody(new StringBuilder(String.valueOf(responseObject))); + } + } + + public abstract OutboundMessageContext getWrappedMessageContext(); + + public void close() { + boolean isLockAcquired = false; + try { + isLockAcquired = GenericHelper.acquireLockIfPossible(SERVLET_GET_IS_OPERATION_LOCK); + if(isLockAcquired) { + HttpRequestHelper.postProcessSecurityHook(this.getClass().getName(), getWrappedMessageContext()); + } + Weaver.callOriginal(); + } finally { + if(isLockAcquired) { + GenericHelper.releaseLock(SERVLET_GET_IS_OPERATION_LOCK); + } + } + } +} diff --git a/instrumentation-security/jersey-2/src/main/java/com/newrelic/agent/security/instrumentation/jersey2/EntityInputStream_Instrumentation.java b/instrumentation-security/jersey-2/src/main/java/com/newrelic/agent/security/instrumentation/jersey2/EntityInputStream_Instrumentation.java new file mode 100644 index 000000000..c4c553d3f --- /dev/null +++ b/instrumentation-security/jersey-2/src/main/java/com/newrelic/agent/security/instrumentation/jersey2/EntityInputStream_Instrumentation.java @@ -0,0 +1,33 @@ +package com.newrelic.agent.security.instrumentation.jersey2; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; + +import java.io.InputStream; + +import static com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper.SERVLET_GET_IS_OPERATION_LOCK; + +@Weave(type = MatchType.ExactClass, originalName = "org.glassfish.jersey.message.internal.EntityInputStream") +class EntityInputStream_Instrumentation { + + protected final InputStream getWrappedStream() { + InputStream retunObject; + boolean isLockAcquired = false; + try { + isLockAcquired = GenericHelper.acquireLockIfPossible(SERVLET_GET_IS_OPERATION_LOCK); + retunObject = Weaver.callOriginal(); + if (isLockAcquired && NewRelicSecurity.isHookProcessingActive() && retunObject != null) { + HttpRequestHelper.registerInputStreamHashIfNeeded(retunObject.hashCode()); + } + } finally { + if(isLockAcquired) { + GenericHelper.releaseLock(SERVLET_GET_IS_OPERATION_LOCK); + } + } + return retunObject; + } + +} diff --git a/instrumentation-security/jersey-2/src/main/java/com/newrelic/agent/security/instrumentation/jersey2/HttpRequestHelper.java b/instrumentation-security/jersey-2/src/main/java/com/newrelic/agent/security/instrumentation/jersey2/HttpRequestHelper.java new file mode 100644 index 000000000..3b56fc9ec --- /dev/null +++ b/instrumentation-security/jersey-2/src/main/java/com/newrelic/agent/security/instrumentation/jersey2/HttpRequestHelper.java @@ -0,0 +1,306 @@ +package com.newrelic.agent.security.instrumentation.jersey2; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.security.instrumentation.helpers.LowSeverityHelper; +import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; +import com.newrelic.api.agent.security.schema.AgentMetaData; +import com.newrelic.api.agent.security.schema.HttpRequest; +import com.newrelic.api.agent.security.schema.SecurityMetaData; +import com.newrelic.api.agent.security.schema.StringUtils; +import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityException; +import com.newrelic.api.agent.security.schema.operation.RXSSOperation; +import com.newrelic.api.agent.security.schema.policy.AgentPolicy; +import org.glassfish.jersey.internal.PropertiesDelegate; +import org.glassfish.jersey.message.internal.OutboundMessageContext; +import org.glassfish.jersey.server.ContainerRequest; + +import javax.ws.rs.core.MultivaluedMap; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; + +public class HttpRequestHelper { + + private static final String X_FORWARDED_FOR = "x-forwarded-for"; + private static final String EMPTY = ""; + public static final String QUESTION_MARK = "?"; + public static final String CONTAINER_RESPONSE_METHOD_NAME = "ContainerResponse"; + + public static final String NR_SEC_CUSTOM_ATTRIB_NAME = "REQUEST_LOCK-"; + private static final String WILDCARD = "*"; + private static final String SEPARATOR = "/"; + public static final String HEADER_SEPARATOR = ";"; + public static final String GRIZZLY_REQUEST_PROPERTIES_DELEGATE = "GRIZZLY_REQUEST_PROPERTIES_DELEGATE"; + public static final String GRIZZLY_REQUEST = "GRIZZLY_REQUEST"; + public static final String ORG_GLASSFISH_JERSEY_GRIZZLY_2_HTTPSERVER_GRIZZLY_REQUEST_PROPERTIES_DELEGATE = "org.glassfish.jersey.grizzly2.httpserver.GrizzlyRequestPropertiesDelegate"; + public static final String ORG_GLASSFISH_GRIZZLY_HTTP_SERVER_REQUEST = "org.glassfish.grizzly.http.server.Request"; + public static final String FIELD_REQUEST = "request"; + public static final String METHOD_GET_REMOTE_ADDR = "getRemoteAddr"; + public static final String METHOD_GET_REMOTE_PORT = "getRemotePort"; + public static final String METHOD_GET_LOCAL_PORT = "getLocalPort"; + public static final String METHOD_GET_SCHEME = "getScheme"; + public static final String METHOD_GET_CONTENT_TYPE = "getContentType"; + public static final String ORG_GLASSFISH_JERSEY_GRIZZLY_2_HTTPSERVER_TRACING_AWARE_PROPERTIES_DELEGATE = "org.glassfish.jersey.message.internal.TracingAwarePropertiesDelegate"; + public static final String TRACING_AWARE_PROPERTIES_DELEGATE = "TRACING_AWARE_PROPERTIES_DELEGATE"; + public static final String FIELD_PROPERTIES_DELEGATE = "propertiesDelegate"; + + private static final String REQUEST_INPUTSTREAM_HASH = "REQUEST_INPUTSTREAM_HASH"; + public static final String CONTENT_TYPE = "content-type"; + public static final String HEADER_CONTENT_TYPE = "contenttype"; + + public static Class grizzlyRequestPropertiesDelegateKlass = null; + + public static Class grizzlyRequest = null; + + public static Class tracingAwarePropertiesDelegateKlass = null; + + public static void preprocessSecurityHook(ContainerRequest requestContext) { + try { + if (!NewRelicSecurity.isHookProcessingActive()) { + return; + } + SecurityMetaData securityMetaData = NewRelicSecurity.getAgent().getSecurityMetaData(); + + HttpRequest securityRequest = securityMetaData.getRequest(); + if (securityRequest.isRequestParsed()) { + return; + } + AgentMetaData securityAgentMetaData = securityMetaData.getMetaData(); + securityRequest.setMethod(requestContext.getMethod()); + HttpRequestHelper.processPropertiesDelegate(requestContext.getPropertiesDelegate(), securityRequest); + + if (securityRequest.getClientIP() != null && !securityRequest.getClientIP().trim().isEmpty()) { + securityAgentMetaData.getIps().add(securityRequest.getClientIP()); + } + HttpRequestHelper.processHttpRequestHeader(requestContext, securityRequest); + + securityMetaData.setTracingHeaderValue(HttpRequestHelper.getTraceHeader(securityRequest.getHeaders())); + securityRequest.setUrl(requestContext.getRequestUri().toString()); + + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + securityMetaData.getMetaData().setServiceTrace(Arrays.copyOfRange(trace, 2, trace.length)); + securityRequest.setRequestParsed(true); + } catch (Throwable ignored) { + } + } + + public static void postProcessSecurityHook(String className, OutboundMessageContext wrappedMessageContext) { + try { + if (!NewRelicSecurity.isHookProcessingActive() + ) { + return; + } + NewRelicSecurity.getAgent().getSecurityMetaData().getResponse().setHeaders(getHeaders(wrappedMessageContext)); + //Add request URI hash to low severity event filter + LowSeverityHelper.addRrequestUriToEventFilter(NewRelicSecurity.getAgent().getSecurityMetaData().getRequest()); + + RXSSOperation rxssOperation = new RXSSOperation(NewRelicSecurity.getAgent().getSecurityMetaData().getRequest(), + NewRelicSecurity.getAgent().getSecurityMetaData().getResponse(), + className, HttpRequestHelper.CONTAINER_RESPONSE_METHOD_NAME); + NewRelicSecurity.getAgent().registerOperation(rxssOperation); + ServletHelper.tmpFileCleanUp(NewRelicSecurity.getAgent().getSecurityMetaData().getFuzzRequestIdentifier().getTempFiles()); + } catch (Throwable e) { + if(e instanceof NewRelicSecurityException){ + e.printStackTrace(); + throw e; + } + } + } + + private static Map getHeaders(OutboundMessageContext outboundMessageContext) { + Map headers = new HashMap<>(); + if(outboundMessageContext == null || outboundMessageContext.getHeaders() == null){ + return headers; + } + for (String key : outboundMessageContext.getStringHeaders().keySet()) { + headers.put(key, outboundMessageContext.getHeaderString(key)); + if(StringUtils.equalsAny(StringUtils.lowerCase(key), CONTENT_TYPE, HEADER_CONTENT_TYPE)){ + NewRelicSecurity.getAgent().getSecurityMetaData().getResponse().setResponseContentType(outboundMessageContext.getHeaderString(key)); + } + } + return headers; + + } + + public static void processHttpRequestHeader(ContainerRequest request, HttpRequest securityRequest){ + MultivaluedMap headers = request.getHeaders(); + for (Map.Entry> header : headers.entrySet()) { + boolean takeNextValue = false; + String headerKey = header.getKey(); + String headerFullValue = getHeaderValue(header.getValue()); + if(headerKey != null){ + headerKey = headerKey.toLowerCase(); + } + AgentPolicy agentPolicy = NewRelicSecurity.getAgent().getCurrentPolicy(); + AgentMetaData agentMetaData = NewRelicSecurity.getAgent().getSecurityMetaData().getMetaData(); + if (agentPolicy != null + && agentPolicy.getProtectionMode().getEnabled() + && agentPolicy.getProtectionMode().getIpBlocking().getEnabled() + && agentPolicy.getProtectionMode().getIpBlocking().getIpDetectViaXFF() + && X_FORWARDED_FOR.equals(headerKey)) { + takeNextValue = true; + } else if (ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID.equals(headerKey)) { + // TODO: May think of removing this intermediate obj and directly create K2 Identifier. + NewRelicSecurity.getAgent().getSecurityMetaData() + .setFuzzRequestIdentifier(ServletHelper.parseFuzzRequestIdentifierHeader(headerFullValue)); + } else if(GenericHelper.CSEC_PARENT_ID.equals(headerKey)) { + NewRelicSecurity.getAgent().getSecurityMetaData() + .addCustomAttribute(GenericHelper.CSEC_PARENT_ID, headerFullValue); + } + + for (String headerValue : header.getValue()) { + if (headerValue != null && !headerValue.trim().isEmpty()) { + if (takeNextValue) { + agentMetaData.setClientDetectedFromXFF(true); + securityRequest.setClientIP(headerValue); + agentMetaData.getIps() + .add(securityRequest.getClientIP()); + securityRequest.setClientPort(EMPTY); + takeNextValue = false; + } + } + } + securityRequest.getHeaders().put(headerKey, headerFullValue); + } + } + + private static String getHeaderValue(List values) { + StringBuilder finalValue = new StringBuilder(); + for (String value : values) { + if (finalValue.length() > 0) { + finalValue.append(HEADER_SEPARATOR); + } + finalValue.append(value); + } + return finalValue.toString(); + } + + public static String getTraceHeader(Map headers) { + String data = EMPTY; + if (headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER) || headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())) { + data = headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER); + if (data == null || data.trim().isEmpty()) { + data = headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase()); + } + } + return data; + } + + public static boolean isRequestLockAcquired() { + try { + return NewRelicSecurity.isHookProcessingActive() && + Boolean.TRUE.equals(NewRelicSecurity.getAgent().getSecurityMetaData().getCustomAttribute(getNrSecCustomAttribName(), Boolean.class)); + } catch (Throwable ignored) {} + return false; + } + + public static boolean acquireRequestLockIfPossible() { + try { + if (NewRelicSecurity.isHookProcessingActive() && + !isRequestLockAcquired()) { + NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(getNrSecCustomAttribName(), true); + return true; + } + } catch (Throwable ignored){} + return false; + } + + public static void releaseRequestLock() { + try { + if(NewRelicSecurity.isHookProcessingActive()) { + NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(getNrSecCustomAttribName(), null); + } + } catch (Throwable ignored){} + } + + private static String getNrSecCustomAttribName() { + return NR_SEC_CUSTOM_ATTRIB_NAME + Thread.currentThread().getId(); + } + + public static void processPropertiesDelegate(PropertiesDelegate propertiesDelegate, HttpRequest securityRequest) { + if(StringUtils.equals(propertiesDelegate.getClass().getName(), ORG_GLASSFISH_JERSEY_GRIZZLY_2_HTTPSERVER_GRIZZLY_REQUEST_PROPERTIES_DELEGATE)){ + try { + Class grizzlyRequestPropertiesDelegateKlass = getClass(GRIZZLY_REQUEST_PROPERTIES_DELEGATE); + Field requestField = grizzlyRequestPropertiesDelegateKlass.getDeclaredField(FIELD_REQUEST); + requestField.setAccessible(true); + Object requestObject = requestField.get(propertiesDelegate); + Class requestClass = getClass(GRIZZLY_REQUEST); + Method getRemoteAddr = requestClass.getDeclaredMethod(METHOD_GET_REMOTE_ADDR); + Method getRemotePort = requestClass.getDeclaredMethod(METHOD_GET_REMOTE_PORT); + Method getLocalPort = requestClass.getDeclaredMethod(METHOD_GET_LOCAL_PORT); + Method getScheme = requestClass.getDeclaredMethod(METHOD_GET_SCHEME); + Method getContentType = requestClass.getDeclaredMethod(METHOD_GET_CONTENT_TYPE); + securityRequest.setClientIP(String.valueOf(getRemoteAddr.invoke(requestObject))); + securityRequest.setClientPort(String.valueOf(getRemotePort.invoke(requestObject))); + securityRequest.setServerPort((int) getLocalPort.invoke(requestObject)); + securityRequest.setProtocol((String) getScheme.invoke(requestObject)); + securityRequest.setContentType((String) getContentType.invoke(requestObject)); + } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException | NoSuchMethodException | + InvocationTargetException e) { + } + + } else if (StringUtils.equals(propertiesDelegate.getClass().getName(), ORG_GLASSFISH_JERSEY_GRIZZLY_2_HTTPSERVER_TRACING_AWARE_PROPERTIES_DELEGATE)){ + try { + Class tracingAwarePropertiesDelegateKlass = getClass(TRACING_AWARE_PROPERTIES_DELEGATE); + Field propertiesDelegateField = tracingAwarePropertiesDelegateKlass.getDeclaredField(FIELD_PROPERTIES_DELEGATE); + propertiesDelegateField.setAccessible(true); + Object propertiesDelegateObject = propertiesDelegateField.get(propertiesDelegate); + processPropertiesDelegate((PropertiesDelegate) propertiesDelegateObject, securityRequest); + } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) { + } + } else { +// System.out.println(propertiesDelegate + " : " + propertiesDelegate.getClass().getName()); + } + } + + private static Class getClass(String klassName) throws ClassNotFoundException { + switch (klassName) { + case GRIZZLY_REQUEST_PROPERTIES_DELEGATE: + if (grizzlyRequestPropertiesDelegateKlass == null) { + grizzlyRequestPropertiesDelegateKlass = Class.forName(ORG_GLASSFISH_JERSEY_GRIZZLY_2_HTTPSERVER_GRIZZLY_REQUEST_PROPERTIES_DELEGATE); + } + return grizzlyRequestPropertiesDelegateKlass; + case GRIZZLY_REQUEST: + if (grizzlyRequest == null) { + grizzlyRequest = Class.forName(ORG_GLASSFISH_GRIZZLY_HTTP_SERVER_REQUEST); + } + return grizzlyRequest; + case TRACING_AWARE_PROPERTIES_DELEGATE: + if (tracingAwarePropertiesDelegateKlass == null) { + tracingAwarePropertiesDelegateKlass = Class.forName(ORG_GLASSFISH_JERSEY_GRIZZLY_2_HTTPSERVER_TRACING_AWARE_PROPERTIES_DELEGATE); + } + return tracingAwarePropertiesDelegateKlass; + default: + throw new ClassNotFoundException(klassName); + } + } + + public static void registerInputStreamHashIfNeeded(int inputStreamHash){ + try { + Set hashSet = NewRelicSecurity.getAgent().getSecurityMetaData().getCustomAttribute(REQUEST_INPUTSTREAM_HASH, Set.class); + if(hashSet == null){ + hashSet = new HashSet<>(); + NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(REQUEST_INPUTSTREAM_HASH, hashSet); + } + hashSet.add(inputStreamHash); + } catch (Throwable ignored) {} + } + + public static void registerUserLevelCode(String frameworkName) { + try { + if (!NewRelicSecurity.isHookProcessingActive() || NewRelicSecurity.getAgent().getSecurityMetaData().getRequest().isEmpty() + ) { + return; + } + SecurityMetaData securityMetaData = NewRelicSecurity.getAgent().getSecurityMetaData(); + if (!securityMetaData.getMetaData().isUserLevelServiceMethodEncountered(frameworkName)) { + securityMetaData.getMetaData().setUserLevelServiceMethodEncountered(true); + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + securityMetaData.getMetaData().setServiceTrace(Arrays.copyOfRange(trace, 2, trace.length)); + } + } catch (Throwable ignored) { + } + } +} diff --git a/instrumentation-security/jetty-11/src/main/java/com/newrelic/agent/security/instrumentation/jetty11/HttpServletHelper.java b/instrumentation-security/jetty-11/src/main/java/com/newrelic/agent/security/instrumentation/jetty11/HttpServletHelper.java index 20e96bcc1..5b415fb5f 100644 --- a/instrumentation-security/jetty-11/src/main/java/com/newrelic/agent/security/instrumentation/jetty11/HttpServletHelper.java +++ b/instrumentation-security/jetty-11/src/main/java/com/newrelic/agent/security/instrumentation/jetty11/HttpServletHelper.java @@ -13,6 +13,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.util.Arrays; import java.util.Enumeration; import java.util.Map; @@ -157,7 +158,8 @@ public static void preprocessSecurityHook(HttpServletRequest httpServletRequest) } securityRequest.setContentType(httpServletRequest.getContentType()); - securityAgentMetaData.setServiceTrace(Thread.currentThread().getStackTrace()); + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + securityMetaData.getMetaData().setServiceTrace(Arrays.copyOfRange(trace, 2, trace.length)); securityRequest.setRequestParsed(true); } catch (Throwable ignored) { } diff --git a/instrumentation-security/jetty-12/build.gradle b/instrumentation-security/jetty-12/build.gradle new file mode 100644 index 000000000..8501cd8aa --- /dev/null +++ b/instrumentation-security/jetty-12/build.gradle @@ -0,0 +1,28 @@ +dependencies { + implementation(project(":newrelic-security-api")) + implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}") + implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}") + implementation("org.eclipse.jetty:jetty-server:12.0.0") + testImplementation('jakarta.servlet:jakarta.servlet-api:6.0.0') + testImplementation("org.eclipse.jetty:jetty-servlet:11.0.16") +} + +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.jetty-12', 'Priority': '10' } +} + +verifyInstrumentation { + passesOnly 'org.eclipse.jetty:jetty-server:[12.0.0,)' + excludeRegex '.*(alpha|beta|rc).*' +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +site { + title 'Jetty' + type 'Appserver' +} \ No newline at end of file diff --git a/instrumentation-security/jetty-12/src/main/java/com/newrelic/agent/security/instrumentation/jetty12/server/HttpServletHelper.java b/instrumentation-security/jetty-12/src/main/java/com/newrelic/agent/security/instrumentation/jetty12/server/HttpServletHelper.java new file mode 100644 index 000000000..71ea403d1 --- /dev/null +++ b/instrumentation-security/jetty-12/src/main/java/com/newrelic/agent/security/instrumentation/jetty12/server/HttpServletHelper.java @@ -0,0 +1,191 @@ +package com.newrelic.agent.security.instrumentation.jetty12.server; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.security.instrumentation.helpers.LowSeverityHelper; +import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; +import com.newrelic.api.agent.security.schema.AgentMetaData; +import com.newrelic.api.agent.security.schema.HttpRequest; +import com.newrelic.api.agent.security.schema.SecurityMetaData; +import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityException; +import com.newrelic.api.agent.security.schema.operation.RXSSOperation; +import com.newrelic.api.agent.security.schema.policy.AgentPolicy; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +public class HttpServletHelper { + + private static final String X_FORWARDED_FOR = "x-forwarded-for"; + private static final String EMPTY = ""; + public static final String QUESTION_MARK = "?"; + public static final String SERVICE_METHOD_NAME = "handle"; + public static final String SERVICE_ASYNC_METHOD_NAME = "handleAsync"; + + public static final String NR_SEC_CUSTOM_ATTRIB_NAME = "JETTY_SERVLET_LOCK-"; + + public static void processHttpRequestHeader(Request request, HttpRequest securityRequest) { + HttpFields headers = request.getHeaders(); + if (headers!=null){ + Set headerKeys = headers.getFieldNamesCollection(); + Iterator headerKeysIterator = headerKeys.iterator(); + while(headerKeysIterator.hasNext()){ + boolean takeNextValue = false; + String headerKey = headerKeysIterator.next(); + if (headerKey != null) { + headerKey = headerKey.toLowerCase(); + } + AgentPolicy agentPolicy = NewRelicSecurity.getAgent().getCurrentPolicy(); + AgentMetaData agentMetaData = NewRelicSecurity.getAgent().getSecurityMetaData().getMetaData(); + if (agentPolicy != null + && agentPolicy.getProtectionMode().getEnabled() + && agentPolicy.getProtectionMode().getIpBlocking().getEnabled() + && agentPolicy.getProtectionMode().getIpBlocking().getIpDetectViaXFF() + && X_FORWARDED_FOR.equals(headerKey)) { + takeNextValue = true; + } else if (ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID.equals(headerKey)) { + // TODO: May think of removing this intermediate obj and directly create K2 Identifier. + NewRelicSecurity.getAgent().getSecurityMetaData().setFuzzRequestIdentifier(ServletHelper.parseFuzzRequestIdentifierHeader(request.getHeaders().get(headerKey))); + } else if(GenericHelper.CSEC_PARENT_ID.equals(headerKey)) { + NewRelicSecurity.getAgent().getSecurityMetaData() + .addCustomAttribute(GenericHelper.CSEC_PARENT_ID, request.getHeaders().get(headerKey)); + } + + String headerFullValue = EMPTY; + String headerValue = request.getHeaders().get(headerKey); + + if (headerValue != null && !headerValue.trim().isEmpty()) { + if (takeNextValue) { + agentMetaData.setClientDetectedFromXFF(true); + securityRequest.setClientIP(headerValue); + agentMetaData.getIps() + .add(securityRequest.getClientIP()); + securityRequest.setClientPort(EMPTY); + } + if (headerFullValue.trim().isEmpty()) { + headerFullValue = headerValue; + } else { + headerFullValue = String.join(";", headerFullValue, headerValue); + } + } + securityRequest.getHeaders().put(headerKey, headerFullValue); + } + } + } + + public static String getTraceHeader(Map headers) { + String data = EMPTY; + if (headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER) || headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())) { + data = headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER); + if (data == null || data.trim().isEmpty()) { + data = headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase()); + } + } + return data; + } + + public static boolean isServletLockAcquired() { + try { + return NewRelicSecurity.isHookProcessingActive() && + Boolean.TRUE.equals(NewRelicSecurity.getAgent().getSecurityMetaData().getCustomAttribute(getNrSecCustomAttribName(), Boolean.class)); + } catch (Throwable ignored) { + } + return false; + } + + public static boolean acquireServletLockIfPossible() { + try { + if (NewRelicSecurity.isHookProcessingActive() && + !isServletLockAcquired()) { + NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(getNrSecCustomAttribName(), true); + return true; + } + } catch (Throwable ignored) { + } + return false; + } + + public static void releaseServletLock() { + try { + if (NewRelicSecurity.isHookProcessingActive()) { + NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(getNrSecCustomAttribName(), null); + } + } catch (Throwable ignored) { + } + } + + private static String getNrSecCustomAttribName() { + return NR_SEC_CUSTOM_ATTRIB_NAME; + } + + public static void preprocessSecurityHook(Request request) { + try { + if (!NewRelicSecurity.isHookProcessingActive() || request == null) { + return; + } + SecurityMetaData securityMetaData = NewRelicSecurity.getAgent().getSecurityMetaData(); + + HttpRequest securityRequest = securityMetaData.getRequest(); + if (securityRequest.isRequestParsed()) { + return; + } + + AgentMetaData securityAgentMetaData = securityMetaData.getMetaData(); + + securityRequest.setMethod(request.getMethod()); + securityRequest.setClientIP(Request.getRemoteAddr(request)); + securityRequest.setServerPort(Request.getLocalPort(request)); + + if (securityRequest.getClientIP() != null && !securityRequest.getClientIP().trim().isEmpty()) { + securityAgentMetaData.getIps().add(securityRequest.getClientIP()); + securityRequest.setClientPort(String.valueOf(Request.getRemotePort(request))); + } + + HttpServletHelper.processHttpRequestHeader(request, securityRequest); + + securityMetaData.setTracingHeaderValue(HttpServletHelper.getTraceHeader(securityRequest.getHeaders())); + + securityRequest.setProtocol(request.getHttpURI().getScheme()); + + // TODO: Create OutBoundHttp data here : Skipping for now. + + String url = request.getHttpURI().asString(); + if (url != null && !url.trim().isEmpty()) { + securityRequest.setUrl(url); + } + securityRequest.setContentType(request.getHeaders().get(HttpHeader.CONTENT_TYPE)); + + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + securityMetaData.getMetaData().setServiceTrace(Arrays.copyOfRange(trace, 2, trace.length)); + securityRequest.setRequestParsed(true); + } catch (Throwable ignored) { + } + } + + public static void postProcessSecurityHook(Request request, Response response, String className, String methodName) { + try { + if (!NewRelicSecurity.isHookProcessingActive() + ) { + return; + } + //Add request URI hash to low severity event filter + LowSeverityHelper.addRrequestUriToEventFilter(NewRelicSecurity.getAgent().getSecurityMetaData().getRequest()); + RXSSOperation rxssOperation = new RXSSOperation(NewRelicSecurity.getAgent().getSecurityMetaData().getRequest(), + NewRelicSecurity.getAgent().getSecurityMetaData().getResponse(), + className, methodName); + NewRelicSecurity.getAgent().registerOperation(rxssOperation); + ServletHelper.tmpFileCleanUp(NewRelicSecurity.getAgent().getSecurityMetaData().getFuzzRequestIdentifier().getTempFiles()); + } catch (Throwable e) { + if (e instanceof NewRelicSecurityException) { + e.printStackTrace(); + throw e; + } + } + } +} diff --git a/instrumentation-security/jetty-12/src/main/java/com/newrelic/agent/security/instrumentation/jetty12/server/RequestHandler_Instrumentation.java b/instrumentation-security/jetty-12/src/main/java/com/newrelic/agent/security/instrumentation/jetty12/server/RequestHandler_Instrumentation.java new file mode 100644 index 000000000..20e5016e3 --- /dev/null +++ b/instrumentation-security/jetty-12/src/main/java/com/newrelic/agent/security/instrumentation/jetty12/server/RequestHandler_Instrumentation.java @@ -0,0 +1,33 @@ +package com.newrelic.agent.security.instrumentation.jetty12.server; + +import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +@Weave(type = MatchType.Interface, originalName = "org.eclipse.jetty.server.Request$Handler") +public class RequestHandler_Instrumentation { + + public boolean handle(Request request, Response response, Callback callback) { + ServletHelper.registerUserLevelCode("jetty-handle"); + boolean isServletLockAcquired = HttpServletHelper.acquireServletLockIfPossible(); + if (isServletLockAcquired) { + HttpServletHelper.preprocessSecurityHook(request); + } + boolean result; + try { + result = Weaver.callOriginal(); + } finally { + if (isServletLockAcquired) { + HttpServletHelper.releaseServletLock(); + } + } + if (isServletLockAcquired) { + HttpServletHelper.postProcessSecurityHook(request, response, this.getClass().getName(), HttpServletHelper.SERVICE_METHOD_NAME); + } + return result; + } +} diff --git a/instrumentation-security/jetty-12/src/test/java/com/nr/agent/security/instrumentation/jetty12/test/MyServlet.java b/instrumentation-security/jetty-12/src/test/java/com/nr/agent/security/instrumentation/jetty12/test/MyServlet.java new file mode 100644 index 000000000..e5c4af3a1 --- /dev/null +++ b/instrumentation-security/jetty-12/src/test/java/com/nr/agent/security/instrumentation/jetty12/test/MyServlet.java @@ -0,0 +1,22 @@ +package com.nr.agent.security.instrumentation.jetty12.test; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +import java.io.BufferedReader; +import java.io.IOException; + +public class MyServlet extends Handler.Abstract { + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + System.out.println("Request completed!"); + callback.succeeded(); + return true; + } +} diff --git a/instrumentation-security/jetty-12/src/test/java/com/nr/agent/security/instrumentation/jetty12/test/ServerTest.java b/instrumentation-security/jetty-12/src/test/java/com/nr/agent/security/instrumentation/jetty12/test/ServerTest.java new file mode 100644 index 000000000..71bcdb51c --- /dev/null +++ b/instrumentation-security/jetty-12/src/test/java/com/nr/agent/security/instrumentation/jetty12/test/ServerTest.java @@ -0,0 +1,347 @@ +package com.nr.agent.security.instrumentation.jetty12.test; + +import com.newrelic.agent.security.introspec.InstrumentationTestConfig; +import com.newrelic.agent.security.introspec.SecurityInstrumentationTestRunner; +import com.newrelic.agent.security.introspec.SecurityIntrospector; +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.AgentMetaData; +import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; +import com.newrelic.api.agent.security.schema.operation.RXSSOperation; +import com.newrelic.security.test.marker.Java11IncompatibleTest; +import com.newrelic.security.test.marker.Java8IncompatibleTest; +import com.newrelic.security.test.marker.Java9IncompatibleTest; +import com.newrelic.agent.security.instrumentation.jetty12.server.HttpServletHelper; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.Callback; +import org.junit.After; +import org.junit.Assert; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.ServerSocket; +import java.net.URL; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@Category({ Java8IncompatibleTest.class, Java9IncompatibleTest.class, Java11IncompatibleTest.class }) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@RunWith(SecurityInstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = {"com.newrelic.agent.security.instrumentation.jetty12.server"}) +public class ServerTest { + public static int PORT = 0; + public static String ENDPOINT = "http://localhost:%d/"; + + private Server server; + + @After + public void teardown() throws Exception { + if (server!=null&&server.isRunning()) { + server.stop(); + } + } + + @Test + public void testHandle() throws Exception { + startWithServlet(); + String headerValue = serviceWithHeaders(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + + AgentMetaData meta = introspector.getSecurityMetaData().getMetaData(); + Assert.assertNotNull("Service trace can not be empty/null.", meta.getServiceTrace()); + Assert.assertTrue("userLevelService method was not encountered.", meta.isUserLevelServiceMethodEncountered()); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + RXSSOperation operation = (RXSSOperation) operations.get(0); + + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.REFLECTED_XSS, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", HttpServletHelper.SERVICE_METHOD_NAME, operation.getMethodName()); + Assert.assertEquals("Wrong client IP detected", "127.0.0.1", operation.getRequest().getClientIP()); + Assert.assertEquals("Wrong Protocol detected", "http", operation.getRequest().getProtocol()); + + Assert.assertEquals("Wrong method", "GET", operation.getRequest().getMethod()); + Assert.assertEquals("Wrong port detected", PORT, operation.getRequest().getServerPort()); + Assert.assertEquals("Wrong Content-type detected", "text/plain", operation.getRequest().getContentType()); + + Map headers = operation.getRequest().getHeaders(); + Assert.assertTrue( + String.format("Missing header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), + headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID) + ); + Assert.assertEquals( + String.format("Invalid header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), + headerValue, + headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID) + ); + Assert.assertTrue( + String.format("Missing header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), + headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase()) + ); + Assert.assertEquals( + String.format("Invalid header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), + headerValue, + headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase()) + ); + } + + @Test + public void testHandle1() throws Exception { + startWithHandler(); + String headerValue = serviceWithHeaders(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + AgentMetaData meta = introspector.getSecurityMetaData().getMetaData(); + Assert.assertNotNull("Service trace can not be empty/null.", meta.getServiceTrace()); + Assert.assertTrue("userLevelService Method was not encountered.", meta.isUserLevelServiceMethodEncountered()); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + RXSSOperation operation = (RXSSOperation) operations.get(0); + + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.REFLECTED_XSS, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", HttpServletHelper.SERVICE_METHOD_NAME, operation.getMethodName()); + Assert.assertEquals("Wrong client IP detected", "127.0.0.1", operation.getRequest().getClientIP()); + Assert.assertEquals("Wrong Protocol detected", "http", operation.getRequest().getProtocol()); + + Assert.assertEquals("Wrong method", "GET", operation.getRequest().getMethod()); + Assert.assertEquals("Wrong port detected", PORT, operation.getRequest().getServerPort()); + Assert.assertEquals("Wrong Content-type detected", "text/plain", operation.getRequest().getContentType()); + + Map headers = operation.getRequest().getHeaders(); + Assert.assertTrue( + String.format("Missing header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), + headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID) + ); + Assert.assertEquals( + String.format("Invalid header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), + headerValue, + headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID) + ); + Assert.assertTrue( + String.format("Missing header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), + headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase()) + ); + Assert.assertEquals( + String.format("Invalid K2 header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), + headerValue, + headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase()) + ); + } + + @Test + public void testHandle2() throws Exception { + startWithServlet(); + serviceWithoutHeaders(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + + AgentMetaData meta = introspector.getSecurityMetaData().getMetaData(); + Assert.assertNotNull("Service trace can not be empty/null.", meta.getServiceTrace()); + Assert.assertTrue("userLevelService method was not encountered.", meta.isUserLevelServiceMethodEncountered()); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + RXSSOperation operation = (RXSSOperation) operations.get(0); + + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.REFLECTED_XSS, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", HttpServletHelper.SERVICE_METHOD_NAME, operation.getMethodName()); + Assert.assertEquals("Wrong client IP detected", "127.0.0.1", operation.getRequest().getClientIP()); + Assert.assertEquals("Wrong Protocol detected", "http", operation.getRequest().getProtocol()); + + Assert.assertEquals("Wrong method", "GET", operation.getRequest().getMethod()); + Assert.assertEquals("Wrong port detected", PORT, operation.getRequest().getServerPort()); + Assert.assertEquals("Wrong Content-type detected", "text/plain", operation.getRequest().getContentType()); + } + + @Test + public void testHandle3() throws Exception { + startWithHandler(); + serviceWithoutHeaders(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + + AgentMetaData meta = introspector.getSecurityMetaData().getMetaData(); + Assert.assertNotNull("Service trace can not be empty/null.", meta.getServiceTrace()); + Assert.assertTrue("userLevelService method was not encountered.", meta.isUserLevelServiceMethodEncountered()); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + RXSSOperation operation = (RXSSOperation) operations.get(0); + + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.REFLECTED_XSS, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", HttpServletHelper.SERVICE_METHOD_NAME, operation.getMethodName()); + Assert.assertEquals("Wrong client IP detected", "127.0.0.1", operation.getRequest().getClientIP()); + Assert.assertEquals("Wrong Protocol detected", "http", operation.getRequest().getProtocol()); + + Assert.assertEquals("Wrong method", "GET", operation.getRequest().getMethod()); + Assert.assertEquals("Wrong port detected", PORT, operation.getRequest().getServerPort()); + Assert.assertEquals("Wrong Content-type detected", "text/plain", operation.getRequest().getContentType()); + } + + @Test + public void testHandle4() throws Exception { + startWithHandlerNonBlocking(); + serviceWithoutHeaders(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + + AgentMetaData meta = introspector.getSecurityMetaData().getMetaData(); + Assert.assertNotNull("Service trace can not be empty/null.", meta.getServiceTrace()); + Assert.assertTrue("userLevelService method was not encountered.", meta.isUserLevelServiceMethodEncountered()); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + RXSSOperation operation = (RXSSOperation) operations.get(0); + + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.REFLECTED_XSS, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", HttpServletHelper.SERVICE_METHOD_NAME, operation.getMethodName()); + Assert.assertEquals("Wrong client IP detected", "127.0.0.1", operation.getRequest().getClientIP()); + Assert.assertEquals("Wrong Protocol detected", "http", operation.getRequest().getProtocol()); + + Assert.assertEquals("Wrong method", "GET", operation.getRequest().getMethod()); + Assert.assertEquals("Wrong port detected", PORT, operation.getRequest().getServerPort()); + Assert.assertEquals("Wrong Content-type detected", "text/plain", operation.getRequest().getContentType()); + } + + @Test + public void testHandle5() throws Exception { + startWithHandlerNonBlocking(); + String headerValue = serviceWithHeaders(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + + AgentMetaData meta = introspector.getSecurityMetaData().getMetaData(); + Assert.assertNotNull("Service trace can not be empty/null.", meta.getServiceTrace()); + Assert.assertTrue("userLevelService method was not encountered.", meta.isUserLevelServiceMethodEncountered()); + + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + RXSSOperation operation = (RXSSOperation) operations.get(0); + + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.REFLECTED_XSS, operation.getCaseType()); + Assert.assertEquals("Invalid executed method name.", HttpServletHelper.SERVICE_METHOD_NAME, operation.getMethodName()); + Assert.assertEquals("Wrong client IP detected", "127.0.0.1", operation.getRequest().getClientIP()); + Assert.assertEquals("Wrong Protocol detected", "http", operation.getRequest().getProtocol()); + + Assert.assertEquals("Wrong method", "GET", operation.getRequest().getMethod()); + Assert.assertEquals("Wrong port detected", PORT, operation.getRequest().getServerPort()); + Assert.assertEquals("Wrong Content-type detected", "text/plain", operation.getRequest().getContentType()); + + Map headers = operation.getRequest().getHeaders(); + Assert.assertTrue( + String.format("Missing header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), + headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID) + ); + Assert.assertEquals( + String.format("Invalid header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), + headerValue, + headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID) + ); + Assert.assertTrue( + String.format("Missing header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), + headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase()) + ); + Assert.assertEquals( + String.format("Invalid header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), + headerValue, + headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase()) + ); + } + + private void startWithServlet() throws Exception { + PORT = getRandomPort(); + Server server = new Server(PORT); +// server.setHandler(new ContextHandler(new MyServlet(), "/testapp/something?ok=34")); + server.setHandler(new MyServlet()); + server.start(); + } + + private void startWithHandler() throws Exception { + PORT = getRandomPort(); + server = new Server(PORT); + server.setHandler( + new Handler.Abstract() { + @Override + public boolean handle (Request request, Response response, Callback callback) throws Exception { + System.out.println("Request 1 completed!"); + callback.succeeded(); + return true; + } + }); + server.start(); + } + + private void startWithHandlerNonBlocking() throws Exception { + PORT = getRandomPort(); + server = new Server(PORT); + server.setHandler( + new Handler.Abstract.NonBlocking() { + @Override + public boolean handle (Request request, Response response, Callback callback) throws Exception { + System.out.println("Request 2 completed!"); + callback.succeeded(); + return true; + } + }); + server.start(); + } + + @Trace(dispatcher = true) + private void serviceWithoutHeaders() throws Exception { + URL u = new URL(String.format(ENDPOINT, PORT)); + HttpURLConnection conn = (HttpURLConnection) u.openConnection(); + + conn.setRequestProperty("content-type", "text/plain;charset=utf-8"); + conn.setRequestMethod("GET"); + conn.connect(); + + System.out.println(conn.getResponseCode()); + waitForProcessing(); + } + + @Trace(dispatcher = true) + private String serviceWithHeaders() throws Exception { + String headerValue = String.valueOf(UUID.randomUUID()); + URL u = new URL(String.format(ENDPOINT, PORT)+"testapp/something?ok=12"); + HttpURLConnection conn = (HttpURLConnection) u.openConnection(); + + conn.setRequestProperty("content-type", "text/plain;charset=utf-8"); + conn.setRequestMethod("GET"); + conn.setRequestProperty(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID, headerValue); + conn.setRequestProperty(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER, headerValue); + conn.connect(); + + conn.getResponseCode(); + waitForProcessing(); + return headerValue; + } + + private static int getRandomPort() { + try (ServerSocket socket = new ServerSocket(0)){ + return socket.getLocalPort(); + } catch (IOException e) { + throw new RuntimeException("Unable to allocate ephemeral port "); + } + } + + private static void waitForProcessing() { + try { + Thread.sleep(50); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/instrumentation-security/jetty-9/src/main/java/com/newrelic/agent/security/instrumentation/jetty9/HttpServletHelper.java b/instrumentation-security/jetty-9/src/main/java/com/newrelic/agent/security/instrumentation/jetty9/HttpServletHelper.java index 68089011e..21cfca19d 100644 --- a/instrumentation-security/jetty-9/src/main/java/com/newrelic/agent/security/instrumentation/jetty9/HttpServletHelper.java +++ b/instrumentation-security/jetty-9/src/main/java/com/newrelic/agent/security/instrumentation/jetty9/HttpServletHelper.java @@ -13,6 +13,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.util.Arrays; import java.util.Enumeration; import java.util.Map; @@ -157,7 +158,8 @@ public static void preprocessSecurityHook(HttpServletRequest httpServletRequest) } securityRequest.setContentType(httpServletRequest.getContentType()); - securityAgentMetaData.setServiceTrace(Thread.currentThread().getStackTrace()); + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + securityMetaData.getMetaData().setServiceTrace(Arrays.copyOfRange(trace, 2, trace.length)); securityRequest.setRequestParsed(true); } catch (Throwable ignored) { } diff --git a/instrumentation-security/lettuce-4.3/build.gradle b/instrumentation-security/lettuce-4.3/build.gradle new file mode 100644 index 000000000..8564b18fc --- /dev/null +++ b/instrumentation-security/lettuce-4.3/build.gradle @@ -0,0 +1,23 @@ +dependencies { + implementation(project(":newrelic-security-api")) + implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}") + implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}") + implementation("biz.paluch.redis:lettuce:4.4.0.Final") + testImplementation("com.github.codemonstur:embedded-redis:1.0.0") +} + +jar { + manifest { + attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.lettuce-4.3' + } +} + +verifyInstrumentation { + passesOnly 'biz.paluch.redis:lettuce:[4.4.0.Final,4.5.0.Final]' + excludeRegex '.*SNAPSHOT' +} + +site { + title 'Lettuce 4.3' + type 'Framework' +} diff --git a/instrumentation-security/lettuce-4.3/src/main/java/com/lambdaworks/redis/AbstractRedisAsyncCommands_Instrumentation.java b/instrumentation-security/lettuce-4.3/src/main/java/com/lambdaworks/redis/AbstractRedisAsyncCommands_Instrumentation.java new file mode 100644 index 000000000..8494f46da --- /dev/null +++ b/instrumentation-security/lettuce-4.3/src/main/java/com/lambdaworks/redis/AbstractRedisAsyncCommands_Instrumentation.java @@ -0,0 +1,98 @@ +/* + * + * * Copyright 2022 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.lambdaworks.redis; + +import com.lambdaworks.redis.api.StatefulConnection; +import com.lambdaworks.redis.protocol.*; +import com.newrelic.agent.security.instrumentation.lettuce_4_3.LettuceUtils; +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityException; +import com.newrelic.api.agent.security.schema.operation.RedisOperation; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; + +import java.util.ArrayList; +import java.util.List; + +@Weave(originalName = "com.lambdaworks.redis.AbstractRedisAsyncCommands") +public abstract class AbstractRedisAsyncCommands_Instrumentation { + + public abstract StatefulConnection getConnection(); + + @SuppressWarnings("unchecked") + @Trace + public AsyncCommand dispatch(RedisCommand_Instrumentation cmd) { + boolean isLockAcquired = acquireLockIfPossible(); + AbstractOperation operation = null; + if(isLockAcquired) { + operation = preprocessSecurityHook(cmd, LettuceUtils.METHOD_DISPATCH); + } + + AsyncCommand returnVal = null; + try { + returnVal = Weaver.callOriginal(); + } finally { + if(isLockAcquired){ + releaseLock(); + } + } + registerExitOperation(isLockAcquired, operation); + + return returnVal; + } + + private void registerExitOperation(boolean isProcessingAllowed, com.newrelic.api.agent.security.schema.AbstractOperation operation) { + try { + if (operation == null || !isProcessingAllowed || !NewRelicSecurity.isHookProcessingActive() || + NewRelicSecurity.getAgent().getSecurityMetaData().getRequest().isEmpty() || GenericHelper.skipExistsEvent() + ) { + return; + } + NewRelicSecurity.getAgent().registerExitEvent(operation); + } catch (Throwable ignored){} + } + + private AbstractOperation preprocessSecurityHook(RedisCommand_Instrumentation cmd, String methodDispatch) { + try { + if (!NewRelicSecurity.isHookProcessingActive() || + NewRelicSecurity.getAgent().getSecurityMetaData().getRequest().isEmpty()){ + return null; + } + String type = cmd.getType().name(); + CommandArgs_Instrumentation commandArgs = cmd.getArgs(); + List arguments = new ArrayList<>(); + for(int i=0 ; i getSingularArgs(CommandArgs_Instrumentation commandArgs) { + return commandArgs.singularArguments; + } + + public static byte[] getByteArgumentVal(CommandArgs.BytesArgument bytesArgument) { + return bytesArgument.val; + } + + public static long getIntegerArgument(CommandArgs.IntegerArgument integerArgument) { + return integerArgument.val; + } + + public static double getDoubleArgument(CommandArgs.DoubleArgument doubleArgument) { + return doubleArgument.val; + } + + public static String getStringArgument(CommandArgs.StringArgument stringArgument) { + return stringArgument.val; + } + + public static char[] getCharArrayArgument(CommandArgs.CharArrayArgument charArrayArgument) { + return charArrayArgument.val; + } + + public static Object getKeyArgument(CommandArgs.KeyArgument keyArgument) { + return keyArgument.key; + } + + public static Object getValueArgument(CommandArgs.ValueArgument valueArgument) { + return valueArgument.val; + } + + public static Object getArgument(Object arg) { + if (arg instanceof CommandArgs.BytesArgument){ + return getByteArgumentVal((CommandArgs.BytesArgument) arg); + } else if (arg instanceof CommandArgs.IntegerArgument) { + return getIntegerArgument((CommandArgs.IntegerArgument) arg); + } else if (arg instanceof CommandArgs.DoubleArgument) { + return getDoubleArgument((CommandArgs.DoubleArgument) arg); + } else if (arg instanceof CommandArgs.StringArgument) { + return getStringArgument((CommandArgs.StringArgument) arg); + } else if (arg instanceof CommandArgs.CharArrayArgument) { + return getCharArrayArgument((CommandArgs.CharArrayArgument) arg); + } else if (arg instanceof CommandArgs.KeyArgument) { + return getKeyArgument((CommandArgs.KeyArgument) arg); + } else if (arg instanceof CommandArgs.ValueArgument) { + return getValueArgument((CommandArgs.ValueArgument) arg); + } + return null; + } +} diff --git a/instrumentation-security/lettuce-4.3/src/main/java/com/lambdaworks/redis/protocol/CommandArgs_Instrumentation.java b/instrumentation-security/lettuce-4.3/src/main/java/com/lambdaworks/redis/protocol/CommandArgs_Instrumentation.java new file mode 100644 index 000000000..8a1add15a --- /dev/null +++ b/instrumentation-security/lettuce-4.3/src/main/java/com/lambdaworks/redis/protocol/CommandArgs_Instrumentation.java @@ -0,0 +1,22 @@ +package com.lambdaworks.redis.protocol; + +import com.newrelic.api.agent.weaver.NewField; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; + +import java.util.ArrayList; +import java.util.List; + +@Weave(originalName = "com.lambdaworks.redis.protocol.CommandArgs") +public class CommandArgs_Instrumentation { + + final List singularArguments = Weaver.callOriginal(); + +// @NewField +// public List singularArguments_instrumentation; + + public int count() { + return Weaver.callOriginal(); + } + +} diff --git a/instrumentation-security/lettuce-4.3/src/main/java/com/lambdaworks/redis/protocol/RedisCommand_Instrumentation.java b/instrumentation-security/lettuce-4.3/src/main/java/com/lambdaworks/redis/protocol/RedisCommand_Instrumentation.java new file mode 100644 index 000000000..b15a4a750 --- /dev/null +++ b/instrumentation-security/lettuce-4.3/src/main/java/com/lambdaworks/redis/protocol/RedisCommand_Instrumentation.java @@ -0,0 +1,15 @@ +package com.lambdaworks.redis.protocol; + +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; + +@Weave(type = MatchType.Interface, originalName = "com.lambdaworks.redis.protocol.RedisCommand") +public abstract class RedisCommand_Instrumentation { + + public abstract ProtocolKeyword getType(); + public CommandArgs_Instrumentation getArgs() { + return Weaver.callOriginal(); + } + +} diff --git a/instrumentation-security/lettuce-4.3/src/main/java/com/newrelic/agent/security/instrumentation/lettuce_4_3/LettuceUtils.java b/instrumentation-security/lettuce-4.3/src/main/java/com/newrelic/agent/security/instrumentation/lettuce_4_3/LettuceUtils.java new file mode 100644 index 000000000..9bee0f18a --- /dev/null +++ b/instrumentation-security/lettuce-4.3/src/main/java/com/newrelic/agent/security/instrumentation/lettuce_4_3/LettuceUtils.java @@ -0,0 +1,13 @@ +package com.newrelic.agent.security.instrumentation.lettuce_4_3; + +public class LettuceUtils { + + public static final String NR_SEC_CUSTOM_ATTRIB_NAME = "REDIS_OPERATION_LOCK_LETTUCE-"; + + public static final String NR_SEC_CUSTOM_ATTR_FILTER_NAME = "REDIS_FILTER-"; + public static final String METHOD_DISPATCH = "dispatch"; + + public static String getNrSecCustomAttribName(int hashCode) { + return NR_SEC_CUSTOM_ATTR_FILTER_NAME + hashCode; + } +} diff --git a/instrumentation-security/lettuce-4.3/src/test/java/com/nr/agent/instrumentation/lettuce_4_3/LettuceTest.java b/instrumentation-security/lettuce-4.3/src/test/java/com/nr/agent/instrumentation/lettuce_4_3/LettuceTest.java new file mode 100644 index 000000000..444200f2a --- /dev/null +++ b/instrumentation-security/lettuce-4.3/src/test/java/com/nr/agent/instrumentation/lettuce_4_3/LettuceTest.java @@ -0,0 +1,520 @@ +package com.nr.agent.instrumentation.lettuce_4_3; + +import com.lambdaworks.redis.AbstractRedisAsyncCommands; +import com.lambdaworks.redis.RedisAsyncCommandsImpl; +import com.lambdaworks.redis.RedisClient; +import com.lambdaworks.redis.api.StatefulRedisConnection; +import com.lambdaworks.redis.api.sync.RedisCommands; +import com.lambdaworks.redis.protocol.CommandType; +import com.newrelic.agent.security.introspec.InstrumentationTestConfig; +import com.newrelic.agent.security.introspec.SecurityInstrumentationTestRunner; +import com.newrelic.agent.security.introspec.SecurityIntrospector; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; +import com.newrelic.api.agent.security.schema.operation.RedisOperation; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; +import redis.embedded.RedisServer; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static com.lambdaworks.redis.protocol.CommandType.SET; + +@RunWith(SecurityInstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = "com.lambdaworks.redis") +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class LettuceTest { + private static RedisServer redisServer; + private static int PORT = 0; + + @BeforeClass + public static void setup() throws Exception { + PORT = getRandomPort(); + redisServer = new RedisServer(PORT); + redisServer.start(); + System.out.println(redisServer); + } + + @Test + public void testSet_Get_Exists_Del() { + KeyValuePair keyValuePair = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + syncCommands.set(keyValuePair.getKey(), keyValuePair.getValue()); + syncCommands.exists(keyValuePair.getKey()); + syncCommands.get(keyValuePair.getKey()); + syncCommands.del(keyValuePair.getKey()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 4); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.SET, operation, Arrays.asList(keyValuePair.getKey(), keyValuePair.getValue())); + + operation = (RedisOperation) operations.get(1); + verifier(CommandType.EXISTS, operation, Collections.singletonList(keyValuePair.getKey())); + + operation = (RedisOperation) operations.get(2); + verifier(CommandType.GET, operation, Collections.singletonList(keyValuePair.getKey())); + + operation = (RedisOperation) operations.get(3); + verifier(CommandType.DEL, operation, Collections.singletonList(keyValuePair.getKey())); + } + + @Test + public void testSetnx_Setex() { + KeyValuePair keyValuePair = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.setnx(keyValuePair.getKey(), keyValuePair.getValue()); + syncCommands.setex(keyValuePair.getKey(), 30, keyValuePair.getValue()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 2); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.SETNX, operation, Arrays.asList(keyValuePair.getKey(), keyValuePair.getValue())); + + operation = (RedisOperation) operations.get(1); + verifier(CommandType.SETEX, operation, Arrays.asList(keyValuePair.getKey(), 30l, keyValuePair.getValue())); + } + + @Test + public void testMsetnx() { + KeyValuePair keyValuePair = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.msetnx(Collections.singletonMap(keyValuePair.getKey(), keyValuePair.getValue())); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 1); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.MSETNX, operation, Arrays.asList(keyValuePair.getKey(), keyValuePair.getValue())); + } + + @Test + public void testHsetnx() { + KeyValuePair keyValuePair = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.hsetnx(keyValuePair.getHash(), keyValuePair.getKey(), keyValuePair.getValue()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 1); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.HSETNX, operation, Arrays.asList(keyValuePair.getHash(), keyValuePair.getKey(), keyValuePair.getValue())); + } + + @Test + public void testHset_Hexists_Hget_Hlen_Hgetall_Hkeys_Hvals_Hdel() { + KeyValuePair keyValuePair = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.hset(keyValuePair.getHash(), keyValuePair.getKey(), keyValuePair.getValue()); + syncCommands.hexists(keyValuePair.getHash(), keyValuePair.getKey()); + syncCommands.hget(keyValuePair.getHash(), keyValuePair.getKey()); + syncCommands.hlen(keyValuePair.getKey()); + syncCommands.hgetall(keyValuePair.getKey()); + syncCommands.hkeys(keyValuePair.getHash()); + syncCommands.hvals(keyValuePair.getHash()); + syncCommands.hdel(keyValuePair.getHash(), keyValuePair.getKey()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 8); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.HSET, operation, Arrays.asList(keyValuePair.getHash(), keyValuePair.getKey(), keyValuePair.getValue())); + + operation = (RedisOperation) operations.get(1); + verifier(CommandType.HEXISTS, operation, Arrays.asList(keyValuePair.getHash(), keyValuePair.getKey())); + + operation = (RedisOperation) operations.get(2); + verifier(CommandType.HGET, operation, Arrays.asList(keyValuePair.getHash(), keyValuePair.getKey())); + + operation = (RedisOperation) operations.get(3); + verifier(CommandType.HLEN, operation, Collections.singletonList(keyValuePair.getKey())); + + operation = (RedisOperation) operations.get(4); + verifier(CommandType.HGETALL, operation, Collections.singletonList(keyValuePair.getKey())); + + operation = (RedisOperation) operations.get(5); + verifier(CommandType.HKEYS, operation, Collections.singletonList(keyValuePair.getHash())); + + operation = (RedisOperation) operations.get(6); + verifier(CommandType.HVALS, operation, Collections.singletonList(keyValuePair.getHash())); + + operation = (RedisOperation) operations.get(7); + verifier(CommandType.HDEL, operation, Arrays.asList(keyValuePair.getHash(), keyValuePair.getKey())); + } + + @Test + public void testMset_Mget() { + KeyValuePair keyValuePair = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.mset(Collections.singletonMap(keyValuePair.getKey(), keyValuePair.getValue())); + syncCommands.mget(keyValuePair.getKey()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 2); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.MSET, operation, Arrays.asList(keyValuePair.getKey(), keyValuePair.getValue())); + + operation = (RedisOperation) operations.get(1); + verifier(CommandType.MGET, operation, Collections.singletonList(keyValuePair.getKey())); + } + + @Test + public void testHmset_Hmget() { + KeyValuePair keyValuePair = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.hmset(keyValuePair.getHash(), Collections.singletonMap(keyValuePair.getKey(), keyValuePair.getValue())); + syncCommands.hmget(keyValuePair.getHash(), keyValuePair.getKey()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 2); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.HMSET, operation, Arrays.asList(keyValuePair.getHash(), keyValuePair.getKey(), keyValuePair.getValue())); + + operation = (RedisOperation) operations.get(1); + verifier(CommandType.HMGET, operation, Arrays.asList(keyValuePair.getHash(), keyValuePair.getKey())); + } + + @Test + public void testIncr_IncrBy_Decr_DecrBy() { + KeyValuePair keyValuePair = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.incr(keyValuePair.getKey()); + syncCommands.incrby(keyValuePair.getKey(), 201); + syncCommands.decr(keyValuePair.getKey()); + syncCommands.decrby(keyValuePair.getKey(), 201); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 4); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.INCR, operation, Collections.singletonList(keyValuePair.getKey())); + + operation = (RedisOperation) operations.get(1); + verifier(CommandType.INCRBY, operation, Arrays.asList(keyValuePair.getKey(), 201l)); + + operation = (RedisOperation) operations.get(2); + verifier(CommandType.DECR, operation, Collections.singletonList(keyValuePair.getKey())); + + operation = (RedisOperation) operations.get(3); + verifier(CommandType.DECRBY, operation, Arrays.asList(keyValuePair.getKey(), 201l)); + } + + @Test + public void testHincrBy() { + KeyValuePair keyValuePair = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.hincrby(keyValuePair.getHash(), keyValuePair.getKey(), 201); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 1); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.HINCRBY, operation, Arrays.asList(keyValuePair.getHash(), keyValuePair.getKey(), 201l)); + } + + @Test + public void testLpush_Llen_Linsert_Lindex_Lpop_Lrem() { + KeyValuePair keyValuePair = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.lpush(keyValuePair.getKey(), keyValuePair.getValue()); + syncCommands.llen(keyValuePair.getKey()); + syncCommands.linsert(keyValuePair.getKey(), true, "0", keyValuePair.getValue()); + syncCommands.lindex(keyValuePair.getKey(), 0); + syncCommands.lpop(keyValuePair.getKey()); + syncCommands.lrem(keyValuePair.getKey(), 1, keyValuePair.getValue()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 6); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.LPUSH, operation, Arrays.asList(keyValuePair.getKey(), keyValuePair.getValue())); + + operation = (RedisOperation) operations.get(1); + verifier(CommandType.LLEN, operation, Collections.singletonList(keyValuePair.getKey())); + + operation = (RedisOperation) operations.get(2); + verifier(CommandType.LINSERT, operation, Arrays.asList(keyValuePair.getKey(), "BEFORE", "0", keyValuePair.getValue())); + + operation = (RedisOperation) operations.get(3); + verifier(CommandType.LINDEX, operation, Arrays.asList(keyValuePair.getKey(), 0l)); + + operation = (RedisOperation) operations.get(4); + verifier(CommandType.LPOP, operation, Collections.singletonList(keyValuePair.getKey())); + + operation = (RedisOperation) operations.get(5); + verifier(CommandType.LREM, operation, Arrays.asList(keyValuePair.getKey(), 1l, keyValuePair.getValue())); + } + + @Test + public void testZadd_Zcard_Zcount_Zincrby_Zrange_Zrem() { + KeyValuePair keyValuePair = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.zadd(keyValuePair.getKey(), 2, keyValuePair.getValue()); + syncCommands.zcard(keyValuePair.getKey()); + syncCommands.zcount(keyValuePair.getKey(), 0, 2); + syncCommands.zincrby(keyValuePair.getKey(), 1, "201"); + syncCommands.zrange(keyValuePair.getKey(), 0, 1); + syncCommands.zrem(keyValuePair.getKey(), "1"); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 6); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.ZADD, operation, Arrays.asList(keyValuePair.getKey(), 2.0d, keyValuePair.getValue())); + + operation = (RedisOperation) operations.get(1); + verifier(CommandType.ZCARD, operation, Collections.singletonList(keyValuePair.getKey())); + + operation = (RedisOperation) operations.get(2); + verifier(CommandType.ZCOUNT, operation, Arrays.asList(keyValuePair.getKey(), "0.0", "2.0")); + + operation = (RedisOperation) operations.get(3); + verifier(CommandType.ZINCRBY, operation, Arrays.asList(keyValuePair.getKey(), 1.0d, "201")); + + operation = (RedisOperation) operations.get(4); + verifier(CommandType.ZRANGE, operation, Arrays.asList(keyValuePair.getKey(), 0l, 1l)); + + operation = (RedisOperation) operations.get(5); + verifier(CommandType.ZREM, operation, Arrays.asList(keyValuePair.getKey(), "1")); + } + + @Test + public void testExpire_Expireat() { + KeyValuePair keyValuePair = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.set(keyValuePair.getKey(), keyValuePair.getValue()); + syncCommands.expire(keyValuePair.getKey(), 30); + long unixTime = System.currentTimeMillis() + 3000; + syncCommands.expireat(keyValuePair.getKey(), unixTime); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 3); + + RedisOperation operation = (RedisOperation) operations.get(1); + verifier(CommandType.EXPIRE, operation, Arrays.asList(keyValuePair.getKey(), 30l)); + + operation = (RedisOperation) operations.get(2); + verifier(CommandType.EXPIREAT, operation, Arrays.asList(keyValuePair.getKey(), unixTime)); + } + + @Test + public void testMulti_Move_Smove_Substr_Exec() { + KeyValuePair keyValuePair1 = KeyValuePair.getKeyValuePair(); + KeyValuePair keyValuePair2 = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.multi(); + syncCommands.smove(keyValuePair1.getKey(), keyValuePair2.getKey(), "member"); + syncCommands.exec(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 1); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.SMOVE, operation, Arrays.asList(keyValuePair1.getKey(), keyValuePair2.getKey(), "member")); + } + + @Test + public void testPing_Quit_Flushdb() { + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.ping(); +// syncCommands.flushDB(); + syncCommands.quit(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + + Assert.assertEquals("Operations detected but was not expecting any.", 0, operations.size()); + } + + @Test + public void testGetSet_Append() { + KeyValuePair keyValuePair = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.getset(keyValuePair.getKey(), "test"); + syncCommands.append(keyValuePair.getKey(), "done"); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 2); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.GETSET, operation, Arrays.asList(keyValuePair.getKey(), "test")); + + operation = (RedisOperation) operations.get(1); + verifier(CommandType.APPEND, operation, Arrays.asList(keyValuePair.getKey(), "done")); + } + + @Test + public void testRpush_Rpushx_Rpop_Rpoplpush() { + KeyValuePair keyValuePair1 = KeyValuePair.getKeyValuePair(); + KeyValuePair keyValuePair2 = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.rpush(keyValuePair1.getKey(), keyValuePair1.getValue()); + syncCommands.rpushx(keyValuePair2.getKey(), keyValuePair2.getValue()); + syncCommands.rpop(keyValuePair1.getKey()); + syncCommands.rpoplpush(keyValuePair2.getKey(), keyValuePair1.getKey()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 4); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.RPUSH, operation, Arrays.asList(keyValuePair1.getKey(), keyValuePair1.getValue())); + + operation = (RedisOperation) operations.get(1); + verifier(CommandType.RPUSHX, operation, Arrays.asList(keyValuePair2.getKey(), keyValuePair2.getValue())); + + operation = (RedisOperation) operations.get(2); + verifier(CommandType.RPOP, operation, Collections.singletonList(keyValuePair1.getKey())); + + operation = (RedisOperation) operations.get(3); + verifier(CommandType.RPOPLPUSH, operation, Arrays.asList(keyValuePair2.getKey(), keyValuePair1.getKey())); + } + + @Test + public void testSadd_Sdiff_Scard_Smove_Srem() { + KeyValuePair keyValuePair1 = KeyValuePair.getKeyValuePair(); + KeyValuePair keyValuePair2 = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.sadd(keyValuePair1.getKey(), keyValuePair1.getValue()); + syncCommands.sdiff(keyValuePair1.getKey(), keyValuePair2.getKey()); + syncCommands.scard(keyValuePair1.getKey()); + syncCommands.smove(keyValuePair1.getKey(), keyValuePair2.getKey(), "test"); + syncCommands.srem(keyValuePair1.getKey(), "test"); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 5); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.SADD, operation, Arrays.asList(keyValuePair1.getKey(), keyValuePair1.getValue())); + + operation = (RedisOperation) operations.get(1); + verifier(CommandType.SDIFF, operation, Arrays.asList(keyValuePair1.getKey(), keyValuePair2.getKey())); + + operation = (RedisOperation) operations.get(2); + verifier(CommandType.SCARD, operation, Collections.singletonList(keyValuePair1.getKey())); + + operation = (RedisOperation) operations.get(3); + verifier(CommandType.SMOVE, operation, Arrays.asList(keyValuePair1.getKey(), keyValuePair2.getKey(), "test")); + + operation = (RedisOperation) operations.get(4); + verifier(CommandType.SREM, operation, Arrays.asList(keyValuePair1.getKey(), "test")); + } + + @AfterClass + public static void tearDown() throws Exception { + redisServer.stop(); + } + + private static void opVerifier(List operations, int expected) { + Assert.assertTrue("No operations detected.", operations.size() > 0); + Assert.assertEquals("Unexpected number of operations detected.", expected, operations.size()); + } + + private static void verifier(CommandType cmd, RedisOperation operation, List keyValuePair) { + Assert.assertEquals(String.format("[%s] Invalid Command.", cmd), cmd.toString(), operation.getType()); + Assert.assertEquals(String.format("[%s] Invalid Category.", cmd), RedisOperation.REDIS, operation.getCategory()); + for(int i=0; i< keyValuePair.size(); i++) { + Assert.assertEquals(String.format("[%s] Invalid executed parameter.", cmd), keyValuePair.get(i), operation.getArguments().get(i)); + } +// Assert.assertEquals(String.format("[%s] Invalid executed parameter.", cmd), keyValuePair, operation.getArguments()); + Assert.assertEquals(String.format("[%s] Invalid event category.", cmd), VulnerabilityCaseType.CACHING_DATA_STORE, operation.getCaseType()); + Assert.assertEquals(String.format("[%s] Invalid executed class name.", cmd), RedisAsyncCommandsImpl.class.getName(), operation.getClassName()); + Assert.assertEquals(String.format("[%s] Invalid executed method name.", cmd), "dispatch", operation.getMethodName()); + } + + private static int getRandomPort() { + int port; + + try { + ServerSocket socket = new ServerSocket(0); + port = socket.getLocalPort(); + socket.close(); + } catch (IOException e) { + throw new RuntimeException("Unable to allocate ephemeral port"); + } + return port; + } + + static class KeyValuePair { + private String key; + private String value; + private String hash; + + public KeyValuePair(String key, String value, String hash) { + this.key = key; + this.value = value; + this.hash = hash; + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + + public String getHash() { + return hash; + } + + public static KeyValuePair getKeyValuePair(){ + UUID uuid = UUID.randomUUID(); + return new KeyValuePair("key-"+uuid, "101", uuid.toString()); + } + } + + private static RedisCommands getStringStringRedisCommands() { + RedisClient redisClient = RedisClient.create("redis://localhost:"+PORT); + StatefulRedisConnection connection = redisClient.connect(); + + RedisCommands syncCommands = connection.sync(); + return syncCommands; + } +} \ No newline at end of file diff --git a/instrumentation-security/lettuce-5.0/build.gradle b/instrumentation-security/lettuce-5.0/build.gradle new file mode 100644 index 000000000..ea06230be --- /dev/null +++ b/instrumentation-security/lettuce-5.0/build.gradle @@ -0,0 +1,24 @@ +dependencies { + implementation(project(":newrelic-security-api")) + implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}") + implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}") + implementation group: 'io.lettuce', name: 'lettuce-core', version: '5.0.3.RELEASE' +// implementation("org.springframework.data:spring-data-redis:3.1.5") + testImplementation("com.github.codemonstur:embedded-redis:1.0.0") +} + +jar { + manifest { + attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.lettuce-5.0' + } +} + +verifyInstrumentation { + passesOnly 'io.lettuce:lettuce-core:[5.0.0.RELEASE,)' + excludeRegex '.*(RC|M).*' +} + +site { + title 'Lettuce 5.0' + type 'Framework' +} diff --git a/instrumentation-security/lettuce-5.0/src/main/java/com/newrelic/agent/security/instrumentation/lettuce_6_0/LettuceUtils.java b/instrumentation-security/lettuce-5.0/src/main/java/com/newrelic/agent/security/instrumentation/lettuce_6_0/LettuceUtils.java new file mode 100644 index 000000000..d058ff186 --- /dev/null +++ b/instrumentation-security/lettuce-5.0/src/main/java/com/newrelic/agent/security/instrumentation/lettuce_6_0/LettuceUtils.java @@ -0,0 +1,13 @@ +package com.newrelic.agent.security.instrumentation.lettuce_6_0; + +public class LettuceUtils { + + public static final String NR_SEC_CUSTOM_ATTRIB_NAME = "REDIS_OPERATION_LOCK_LETTUCE-"; + + public static final String NR_SEC_CUSTOM_ATTR_FILTER_NAME = "REDIS_FILTER-"; + public static final String METHOD_DISPATCH = "dispatch"; + + public static String getNrSecCustomAttribName(int hashCode) { + return NR_SEC_CUSTOM_ATTR_FILTER_NAME + hashCode; + } +} diff --git a/instrumentation-security/lettuce-5.0/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands_Instrumentation.java b/instrumentation-security/lettuce-5.0/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands_Instrumentation.java new file mode 100644 index 000000000..36025d720 --- /dev/null +++ b/instrumentation-security/lettuce-5.0/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands_Instrumentation.java @@ -0,0 +1,101 @@ +/* + * + * * Copyright 2022 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package io.lettuce.core; + +import com.newrelic.agent.security.instrumentation.lettuce_6_0.LettuceUtils; +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityException; +import com.newrelic.api.agent.security.schema.operation.RedisOperation; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import io.lettuce.core.api.StatefulConnection; +import io.lettuce.core.protocol.AsyncCommand; +import io.lettuce.core.protocol.CommandArgsCsecUtils; +import io.lettuce.core.protocol.CommandArgs_Instrumentation; +import io.lettuce.core.protocol.RedisCommand_Instrumentation; + +import java.util.ArrayList; +import java.util.List; + +@Weave(originalName = "io.lettuce.core.AbstractRedisAsyncCommands") +public abstract class AbstractRedisAsyncCommands_Instrumentation { + + public abstract StatefulConnection getConnection(); + + @SuppressWarnings("unchecked") + @Trace + public AsyncCommand dispatch(RedisCommand_Instrumentation cmd) { + boolean isLockAcquired = acquireLockIfPossible(); + AbstractOperation operation = null; + if(isLockAcquired) { + operation = preprocessSecurityHook(cmd, LettuceUtils.METHOD_DISPATCH); + } + + AsyncCommand returnVal = null; + try { + returnVal = Weaver.callOriginal(); + } finally { + if(isLockAcquired){ + releaseLock(); + } + } + registerExitOperation(isLockAcquired, operation); + + return returnVal; + } + + private void registerExitOperation(boolean isProcessingAllowed, com.newrelic.api.agent.security.schema.AbstractOperation operation) { + try { + if (operation == null || !isProcessingAllowed || !NewRelicSecurity.isHookProcessingActive() || + NewRelicSecurity.getAgent().getSecurityMetaData().getRequest().isEmpty() || GenericHelper.skipExistsEvent() + ) { + return; + } + NewRelicSecurity.getAgent().registerExitEvent(operation); + } catch (Throwable ignored){} + } + + private AbstractOperation preprocessSecurityHook(RedisCommand_Instrumentation cmd, String methodDispatch) { + try { + if (!NewRelicSecurity.isHookProcessingActive() || + NewRelicSecurity.getAgent().getSecurityMetaData().getRequest().isEmpty()){ + return null; + } + String type = cmd.getType().name(); + CommandArgs_Instrumentation commandArgs = cmd.getArgs(); + List arguments = new ArrayList<>(); + for(int i=0 ; i getSingularArgs(CommandArgs_Instrumentation commandArgs) { + return commandArgs.singularArguments; + } + + public static byte[] getByteArgumentVal(CommandArgs.BytesArgument bytesArgument) { + return bytesArgument.val; + } + + public static long getIntegerArgument(CommandArgs.IntegerArgument integerArgument) { + return integerArgument.val; + } + + public static double getDoubleArgument(CommandArgs.DoubleArgument doubleArgument) { + return doubleArgument.val; + } + + public static String getStringArgument(CommandArgs.StringArgument stringArgument) { + return stringArgument.val; + } + + public static char[] getCharArrayArgument(CommandArgs.CharArrayArgument charArrayArgument) { + return charArrayArgument.val; + } + + public static Object getKeyArgument(CommandArgs.KeyArgument keyArgument) { + return keyArgument.key; + } + + public static Object getValueArgument(CommandArgs.ValueArgument valueArgument) { + return valueArgument.val; + } + + public static Object getSpringDataArgument(Object argument){ + Object returnValue = null; + try { + returnValue = NewRelicSecurity.getAgent().getSecurityMetaData().getCustomAttribute(GenericHelper.NR_SEC_CUSTOM_SPRING_REDIS_ATTR + argument.hashCode(), Object.class); + } catch (Exception e){ + returnValue = argument; + } + return (returnValue!=null)?returnValue:argument; + } + + public static Object getArgument(Object arg) { + Object argument = null; + if (arg instanceof CommandArgs.BytesArgument){ + argument = getByteArgumentVal((CommandArgs.BytesArgument) arg); + } else if (arg instanceof CommandArgs.IntegerArgument) { + argument = getIntegerArgument((CommandArgs.IntegerArgument) arg); + } else if (arg instanceof CommandArgs.DoubleArgument) { + argument = getDoubleArgument((CommandArgs.DoubleArgument) arg); + } else if (arg instanceof CommandArgs.StringArgument) { + argument = getStringArgument((CommandArgs.StringArgument) arg); + } else if (arg instanceof CommandArgs.CharArrayArgument) { + argument = getCharArrayArgument((CommandArgs.CharArrayArgument) arg); + } else if (arg instanceof CommandArgs.KeyArgument) { + argument = getKeyArgument((CommandArgs.KeyArgument) arg); + } else if (arg instanceof CommandArgs.ValueArgument) { + argument = getValueArgument((CommandArgs.ValueArgument) arg); + } + if(argument != null) { + return getSpringDataArgument(argument); + } else { + return null; + } + } +} diff --git a/instrumentation-security/lettuce-5.0/src/main/java/io/lettuce/core/protocol/CommandArgs_Instrumentation.java b/instrumentation-security/lettuce-5.0/src/main/java/io/lettuce/core/protocol/CommandArgs_Instrumentation.java new file mode 100644 index 000000000..50bf76c66 --- /dev/null +++ b/instrumentation-security/lettuce-5.0/src/main/java/io/lettuce/core/protocol/CommandArgs_Instrumentation.java @@ -0,0 +1,22 @@ +package io.lettuce.core.protocol; + +import com.newrelic.api.agent.weaver.NewField; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; + +import java.util.ArrayList; +import java.util.List; + +@Weave(originalName = "io.lettuce.core.protocol.CommandArgs") +public class CommandArgs_Instrumentation { + + final List singularArguments = Weaver.callOriginal(); + +// @NewField +// public List singularArguments_instrumentation; + + public int count() { + return Weaver.callOriginal(); + } + +} diff --git a/instrumentation-security/lettuce-5.0/src/main/java/io/lettuce/core/protocol/RedisCommand_Instrumentation.java b/instrumentation-security/lettuce-5.0/src/main/java/io/lettuce/core/protocol/RedisCommand_Instrumentation.java new file mode 100644 index 000000000..e0f355b4b --- /dev/null +++ b/instrumentation-security/lettuce-5.0/src/main/java/io/lettuce/core/protocol/RedisCommand_Instrumentation.java @@ -0,0 +1,15 @@ +package io.lettuce.core.protocol; + +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; + +@Weave(type = MatchType.Interface, originalName = "io.lettuce.core.protocol.RedisCommand") +public abstract class RedisCommand_Instrumentation { + + public abstract ProtocolKeyword getType(); + public CommandArgs_Instrumentation getArgs() { + return Weaver.callOriginal(); + } + +} diff --git a/instrumentation-security/lettuce-5.0/src/test/java/com/nr/agent/instrumentation/lettuce_5/LettuceTest.java b/instrumentation-security/lettuce-5.0/src/test/java/com/nr/agent/instrumentation/lettuce_5/LettuceTest.java new file mode 100644 index 000000000..c91d70f09 --- /dev/null +++ b/instrumentation-security/lettuce-5.0/src/test/java/com/nr/agent/instrumentation/lettuce_5/LettuceTest.java @@ -0,0 +1,517 @@ +package com.nr.agent.instrumentation.lettuce_5; + +import com.newrelic.agent.security.introspec.InstrumentationTestConfig; +import com.newrelic.agent.security.introspec.SecurityInstrumentationTestRunner; +import com.newrelic.agent.security.introspec.SecurityIntrospector; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; +import com.newrelic.api.agent.security.schema.operation.RedisOperation; +import io.lettuce.core.RedisAsyncCommandsImpl; +import io.lettuce.core.RedisClient; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.sync.RedisCommands; +import io.lettuce.core.protocol.CommandType; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; +import redis.embedded.RedisServer; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +@RunWith(SecurityInstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = "io.lettuce.core") +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class LettuceTest { + private static RedisServer redisServer; + private static int PORT = 0; + + @BeforeClass + public static void setup() throws Exception { + PORT = getRandomPort(); + redisServer = new RedisServer(PORT); + redisServer.start(); + System.out.println(redisServer); + } + + @Test + public void testSet_Get_Exists_Del() { + KeyValuePair keyValuePair = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + syncCommands.set(keyValuePair.getKey(), keyValuePair.getValue()); + syncCommands.exists(keyValuePair.getKey()); + syncCommands.get(keyValuePair.getKey()); + syncCommands.del(keyValuePair.getKey()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 4); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.SET, operation, Arrays.asList(keyValuePair.getKey(), keyValuePair.getValue())); + + operation = (RedisOperation) operations.get(1); + verifier(CommandType.EXISTS, operation, Collections.singletonList(keyValuePair.getKey())); + + operation = (RedisOperation) operations.get(2); + verifier(CommandType.GET, operation, Collections.singletonList(keyValuePair.getKey())); + + operation = (RedisOperation) operations.get(3); + verifier(CommandType.DEL, operation, Collections.singletonList(keyValuePair.getKey())); + } + + @Test + public void testSetnx_Setex() { + KeyValuePair keyValuePair = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.setnx(keyValuePair.getKey(), keyValuePair.getValue()); + syncCommands.setex(keyValuePair.getKey(), 30, keyValuePair.getValue()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 2); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.SETNX, operation, Arrays.asList(keyValuePair.getKey(), keyValuePair.getValue())); + + operation = (RedisOperation) operations.get(1); + verifier(CommandType.SETEX, operation, Arrays.asList(keyValuePair.getKey(), 30l, keyValuePair.getValue())); + } + + @Test + public void testMsetnx() { + KeyValuePair keyValuePair = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.msetnx(Collections.singletonMap(keyValuePair.getKey(), keyValuePair.getValue())); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 1); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.MSETNX, operation, Arrays.asList(keyValuePair.getKey(), keyValuePair.getValue())); + } + + @Test + public void testHsetnx() { + KeyValuePair keyValuePair = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.hsetnx(keyValuePair.getHash(), keyValuePair.getKey(), keyValuePair.getValue()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 1); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.HSETNX, operation, Arrays.asList(keyValuePair.getHash(), keyValuePair.getKey(), keyValuePair.getValue())); + } + + @Test + public void testHset_Hexists_Hget_Hlen_Hgetall_Hkeys_Hvals_Hdel() { + KeyValuePair keyValuePair = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.hset(keyValuePair.getHash(), keyValuePair.getKey(), keyValuePair.getValue()); + syncCommands.hexists(keyValuePair.getHash(), keyValuePair.getKey()); + syncCommands.hget(keyValuePair.getHash(), keyValuePair.getKey()); + syncCommands.hlen(keyValuePair.getKey()); + syncCommands.hgetall(keyValuePair.getKey()); + syncCommands.hkeys(keyValuePair.getHash()); + syncCommands.hvals(keyValuePair.getHash()); + syncCommands.hdel(keyValuePair.getHash(), keyValuePair.getKey()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 8); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.HSET, operation, Arrays.asList(keyValuePair.getHash(), keyValuePair.getKey(), keyValuePair.getValue())); + + operation = (RedisOperation) operations.get(1); + verifier(CommandType.HEXISTS, operation, Arrays.asList(keyValuePair.getHash(), keyValuePair.getKey())); + + operation = (RedisOperation) operations.get(2); + verifier(CommandType.HGET, operation, Arrays.asList(keyValuePair.getHash(), keyValuePair.getKey())); + + operation = (RedisOperation) operations.get(3); + verifier(CommandType.HLEN, operation, Collections.singletonList(keyValuePair.getKey())); + + operation = (RedisOperation) operations.get(4); + verifier(CommandType.HGETALL, operation, Collections.singletonList(keyValuePair.getKey())); + + operation = (RedisOperation) operations.get(5); + verifier(CommandType.HKEYS, operation, Collections.singletonList(keyValuePair.getHash())); + + operation = (RedisOperation) operations.get(6); + verifier(CommandType.HVALS, operation, Collections.singletonList(keyValuePair.getHash())); + + operation = (RedisOperation) operations.get(7); + verifier(CommandType.HDEL, operation, Arrays.asList(keyValuePair.getHash(), keyValuePair.getKey())); + } + + @Test + public void testMset_Mget() { + KeyValuePair keyValuePair = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.mset(Collections.singletonMap(keyValuePair.getKey(), keyValuePair.getValue())); + syncCommands.mget(keyValuePair.getKey()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 2); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.MSET, operation, Arrays.asList(keyValuePair.getKey(), keyValuePair.getValue())); + + operation = (RedisOperation) operations.get(1); + verifier(CommandType.MGET, operation, Collections.singletonList(keyValuePair.getKey())); + } + + @Test + public void testHmset_Hmget() { + KeyValuePair keyValuePair = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.hmset(keyValuePair.getHash(), Collections.singletonMap(keyValuePair.getKey(), keyValuePair.getValue())); + syncCommands.hmget(keyValuePair.getHash(), keyValuePair.getKey()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 2); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.HMSET, operation, Arrays.asList(keyValuePair.getHash(), keyValuePair.getKey(), keyValuePair.getValue())); + + operation = (RedisOperation) operations.get(1); + verifier(CommandType.HMGET, operation, Arrays.asList(keyValuePair.getHash(), keyValuePair.getKey())); + } + + @Test + public void testIncr_IncrBy_Decr_DecrBy() { + KeyValuePair keyValuePair = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.incr(keyValuePair.getKey()); + syncCommands.incrby(keyValuePair.getKey(), 201); + syncCommands.decr(keyValuePair.getKey()); + syncCommands.decrby(keyValuePair.getKey(), 201); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 4); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.INCR, operation, Collections.singletonList(keyValuePair.getKey())); + + operation = (RedisOperation) operations.get(1); + verifier(CommandType.INCRBY, operation, Arrays.asList(keyValuePair.getKey(), 201l)); + + operation = (RedisOperation) operations.get(2); + verifier(CommandType.DECR, operation, Collections.singletonList(keyValuePair.getKey())); + + operation = (RedisOperation) operations.get(3); + verifier(CommandType.DECRBY, operation, Arrays.asList(keyValuePair.getKey(), 201l)); + } + + @Test + public void testHincrBy() { + KeyValuePair keyValuePair = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.hincrby(keyValuePair.getHash(), keyValuePair.getKey(), 201); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 1); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.HINCRBY, operation, Arrays.asList(keyValuePair.getHash(), keyValuePair.getKey(), 201l)); + } + + @Test + public void testLpush_Llen_Linsert_Lindex_Lpop_Lrem() { + KeyValuePair keyValuePair = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.lpush(keyValuePair.getKey(), keyValuePair.getValue()); + syncCommands.llen(keyValuePair.getKey()); + syncCommands.linsert(keyValuePair.getKey(), true, "0", keyValuePair.getValue()); + syncCommands.lindex(keyValuePair.getKey(), 0); + syncCommands.lpop(keyValuePair.getKey()); + syncCommands.lrem(keyValuePair.getKey(), 1, keyValuePair.getValue()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 6); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.LPUSH, operation, Arrays.asList(keyValuePair.getKey(), keyValuePair.getValue())); + + operation = (RedisOperation) operations.get(1); + verifier(CommandType.LLEN, operation, Collections.singletonList(keyValuePair.getKey())); + + operation = (RedisOperation) operations.get(2); + verifier(CommandType.LINSERT, operation, Arrays.asList(keyValuePair.getKey(), "BEFORE", "0", keyValuePair.getValue())); + + operation = (RedisOperation) operations.get(3); + verifier(CommandType.LINDEX, operation, Arrays.asList(keyValuePair.getKey(), 0l)); + + operation = (RedisOperation) operations.get(4); + verifier(CommandType.LPOP, operation, Collections.singletonList(keyValuePair.getKey())); + + operation = (RedisOperation) operations.get(5); + verifier(CommandType.LREM, operation, Arrays.asList(keyValuePair.getKey(), 1l, keyValuePair.getValue())); + } + + @Test + public void testZadd_Zcard_Zcount_Zincrby_Zrange_Zrem() { + KeyValuePair keyValuePair = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.zadd(keyValuePair.getKey(), 2, keyValuePair.getValue()); + syncCommands.zcard(keyValuePair.getKey()); + syncCommands.zcount(keyValuePair.getKey(), 0, 2); + syncCommands.zincrby(keyValuePair.getKey(), 1, "201"); + syncCommands.zrange(keyValuePair.getKey(), 0, 1); + syncCommands.zrem(keyValuePair.getKey(), "1"); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 6); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.ZADD, operation, Arrays.asList(keyValuePair.getKey(), 2.0d, keyValuePair.getValue())); + + operation = (RedisOperation) operations.get(1); + verifier(CommandType.ZCARD, operation, Collections.singletonList(keyValuePair.getKey())); + + operation = (RedisOperation) operations.get(2); + verifier(CommandType.ZCOUNT, operation, Arrays.asList(keyValuePair.getKey(), "0.0", "2.0")); + + operation = (RedisOperation) operations.get(3); + verifier(CommandType.ZINCRBY, operation, Arrays.asList(keyValuePair.getKey(), 1.0d, "201")); + + operation = (RedisOperation) operations.get(4); + verifier(CommandType.ZRANGE, operation, Arrays.asList(keyValuePair.getKey(), 0l, 1l)); + + operation = (RedisOperation) operations.get(5); + verifier(CommandType.ZREM, operation, Arrays.asList(keyValuePair.getKey(), "1")); + } + + @Test + public void testExpire_Expireat() { + KeyValuePair keyValuePair = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.set(keyValuePair.getKey(), keyValuePair.getValue()); + syncCommands.expire(keyValuePair.getKey(), 30); + long unixTime = System.currentTimeMillis() + 3000; + syncCommands.expireat(keyValuePair.getKey(), unixTime); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 3); + + RedisOperation operation = (RedisOperation) operations.get(1); + verifier(CommandType.EXPIRE, operation, Arrays.asList(keyValuePair.getKey(), 30l)); + + operation = (RedisOperation) operations.get(2); + verifier(CommandType.EXPIREAT, operation, Arrays.asList(keyValuePair.getKey(), unixTime)); + } + + @Test + public void testMulti_Move_Smove_Substr_Exec() { + KeyValuePair keyValuePair1 = KeyValuePair.getKeyValuePair(); + KeyValuePair keyValuePair2 = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.multi(); + syncCommands.smove(keyValuePair1.getKey(), keyValuePair2.getKey(), "member"); + syncCommands.exec(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 1); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.SMOVE, operation, Arrays.asList(keyValuePair1.getKey(), keyValuePair2.getKey(), "member")); + } + + @Test + public void testPing_Quit_Flushdb() { + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.ping(); +// syncCommands.flushDB(); + syncCommands.quit(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + + Assert.assertEquals("Operations detected but was not expecting any.", 0, operations.size()); + } + + @Test + public void testGetSet_Append() { + KeyValuePair keyValuePair = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.getset(keyValuePair.getKey(), "test"); + syncCommands.append(keyValuePair.getKey(), "done"); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 2); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.GETSET, operation, Arrays.asList(keyValuePair.getKey(), "test")); + + operation = (RedisOperation) operations.get(1); + verifier(CommandType.APPEND, operation, Arrays.asList(keyValuePair.getKey(), "done")); + } + + @Test + public void testRpush_Rpushx_Rpop_Rpoplpush() { + KeyValuePair keyValuePair1 = KeyValuePair.getKeyValuePair(); + KeyValuePair keyValuePair2 = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.rpush(keyValuePair1.getKey(), keyValuePair1.getValue()); + syncCommands.rpushx(keyValuePair2.getKey(), keyValuePair2.getValue()); + syncCommands.rpop(keyValuePair1.getKey()); + syncCommands.rpoplpush(keyValuePair2.getKey(), keyValuePair1.getKey()); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 4); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.RPUSH, operation, Arrays.asList(keyValuePair1.getKey(), keyValuePair1.getValue())); + + operation = (RedisOperation) operations.get(1); + verifier(CommandType.RPUSHX, operation, Arrays.asList(keyValuePair2.getKey(), keyValuePair2.getValue())); + + operation = (RedisOperation) operations.get(2); + verifier(CommandType.RPOP, operation, Collections.singletonList(keyValuePair1.getKey())); + + operation = (RedisOperation) operations.get(3); + verifier(CommandType.RPOPLPUSH, operation, Arrays.asList(keyValuePair2.getKey(), keyValuePair1.getKey())); + } + + @Test + public void testSadd_Sdiff_Scard_Smove_Srem() { + KeyValuePair keyValuePair1 = KeyValuePair.getKeyValuePair(); + KeyValuePair keyValuePair2 = KeyValuePair.getKeyValuePair(); + RedisCommands syncCommands = getStringStringRedisCommands(); + + syncCommands.sadd(keyValuePair1.getKey(), keyValuePair1.getValue()); + syncCommands.sdiff(keyValuePair1.getKey(), keyValuePair2.getKey()); + syncCommands.scard(keyValuePair1.getKey()); + syncCommands.smove(keyValuePair1.getKey(), keyValuePair2.getKey(), "test"); + syncCommands.srem(keyValuePair1.getKey(), "test"); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + opVerifier(operations, 5); + + RedisOperation operation = (RedisOperation) operations.get(0); + verifier(CommandType.SADD, operation, Arrays.asList(keyValuePair1.getKey(), keyValuePair1.getValue())); + + operation = (RedisOperation) operations.get(1); + verifier(CommandType.SDIFF, operation, Arrays.asList(keyValuePair1.getKey(), keyValuePair2.getKey())); + + operation = (RedisOperation) operations.get(2); + verifier(CommandType.SCARD, operation, Collections.singletonList(keyValuePair1.getKey())); + + operation = (RedisOperation) operations.get(3); + verifier(CommandType.SMOVE, operation, Arrays.asList(keyValuePair1.getKey(), keyValuePair2.getKey(), "test")); + + operation = (RedisOperation) operations.get(4); + verifier(CommandType.SREM, operation, Arrays.asList(keyValuePair1.getKey(), "test")); + } + + @AfterClass + public static void tearDown() throws Exception { + redisServer.stop(); + } + + private static void opVerifier(List operations, int expected) { + Assert.assertTrue("No operations detected.", operations.size() > 0); + Assert.assertEquals("Unexpected number of operations detected.", expected, operations.size()); + } + + private static void verifier(CommandType cmd, RedisOperation operation, List keyValuePair) { + Assert.assertEquals(String.format("[%s] Invalid Command.", cmd), cmd.toString(), operation.getType()); + Assert.assertEquals(String.format("[%s] Invalid Category.", cmd), RedisOperation.REDIS, operation.getCategory()); + for(int i=0; i< keyValuePair.size(); i++) { + Assert.assertEquals(String.format("[%s] Invalid executed parameter.", cmd), keyValuePair.get(i), operation.getArguments().get(i)); + } +// Assert.assertEquals(String.format("[%s] Invalid executed parameter.", cmd), keyValuePair, operation.getArguments()); + Assert.assertEquals(String.format("[%s] Invalid event category.", cmd), VulnerabilityCaseType.CACHING_DATA_STORE, operation.getCaseType()); + Assert.assertEquals(String.format("[%s] Invalid executed class name.", cmd), RedisAsyncCommandsImpl.class.getName(), operation.getClassName()); + Assert.assertEquals(String.format("[%s] Invalid executed method name.", cmd), "dispatch", operation.getMethodName()); + } + + private static int getRandomPort() { + int port; + + try { + ServerSocket socket = new ServerSocket(0); + port = socket.getLocalPort(); + socket.close(); + } catch (IOException e) { + throw new RuntimeException("Unable to allocate ephemeral port"); + } + return port; + } + + static class KeyValuePair { + private String key; + private String value; + private String hash; + + public KeyValuePair(String key, String value, String hash) { + this.key = key; + this.value = value; + this.hash = hash; + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + + public String getHash() { + return hash; + } + + public static KeyValuePair getKeyValuePair(){ + UUID uuid = UUID.randomUUID(); + return new KeyValuePair("key-"+uuid, "101", uuid.toString()); + } + } + + private static RedisCommands getStringStringRedisCommands() { + RedisClient redisClient = RedisClient.create("redis://localhost:"+PORT); + StatefulRedisConnection connection = redisClient.connect(); + + RedisCommands syncCommands = connection.sync(); + return syncCommands; + } +} diff --git a/instrumentation-security/low-priority-instrumentation/build.gradle b/instrumentation-security/low-priority-instrumentation/build.gradle index 8e12a345f..786c60c1a 100644 --- a/instrumentation-security/low-priority-instrumentation/build.gradle +++ b/instrumentation-security/low-priority-instrumentation/build.gradle @@ -11,7 +11,7 @@ dependencies { jar { manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.low-priority-instrumentation', - 'Enabled': 'false' } + 'Enabled': 'true' } } verifyInstrumentation { diff --git a/instrumentation-security/netty-4.0.0/src/main/java/security/io/netty400/channel/ChannelInboundHandler_Instrumentation.java b/instrumentation-security/netty-4.0.0/src/main/java/security/io/netty400/channel/ChannelInboundHandler_Instrumentation.java index c493a74a2..4fa642234 100644 --- a/instrumentation-security/netty-4.0.0/src/main/java/security/io/netty400/channel/ChannelInboundHandler_Instrumentation.java +++ b/instrumentation-security/netty-4.0.0/src/main/java/security/io/netty400/channel/ChannelInboundHandler_Instrumentation.java @@ -19,10 +19,19 @@ public abstract class ChannelInboundHandler_Instrumentation { public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - NettyUtils.processSecurityRequest(ctx, msg, getClass().getName()); - if (!StringUtils.startsWith(getClass().getName(), NettyUtils.IO_NETTY)) { - ServletHelper.registerUserLevelCode(NettyUtils.IO_NETTY); + boolean isLockAcquired = NettyUtils.acquireNettyLockIfPossible(); + if (isLockAcquired) { + NettyUtils.processSecurityRequest(ctx, msg, getClass().getName()); + if (!StringUtils.startsWith(getClass().getName(), NettyUtils.IO_NETTY)) { + ServletHelper.registerUserLevelCode(NettyUtils.IO_NETTY); + } + } + try { + Weaver.callOriginal(); + } finally { + if (isLockAcquired) { + NettyUtils.releaseNettyLock(); + } } - Weaver.callOriginal(); } } diff --git a/instrumentation-security/netty-4.0.0/src/main/java/security/io/netty400/channel/ChannelOutboundHandler_Instrumentation.java b/instrumentation-security/netty-4.0.0/src/main/java/security/io/netty400/channel/ChannelOutboundHandler_Instrumentation.java index 444ca9260..5990decd6 100644 --- a/instrumentation-security/netty-4.0.0/src/main/java/security/io/netty400/channel/ChannelOutboundHandler_Instrumentation.java +++ b/instrumentation-security/netty-4.0.0/src/main/java/security/io/netty400/channel/ChannelOutboundHandler_Instrumentation.java @@ -18,8 +18,17 @@ public abstract class ChannelOutboundHandler_Instrumentation { public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { - NettyUtils.processSecurityResponse(ctx, msg); - NettyUtils.sendRXSSEvent(ctx, msg, getClass().getName(), NettyUtils.WRITE_METHOD_NAME); - Weaver.callOriginal(); + boolean isLockAcquired = NettyUtils.acquireNettyLockIfPossible(); + if (isLockAcquired) { + NettyUtils.processSecurityResponse(ctx, msg); + NettyUtils.sendRXSSEvent(ctx, msg, getClass().getName(), NettyUtils.WRITE_METHOD_NAME); + } + try { + Weaver.callOriginal(); + } finally { + if (isLockAcquired) { + NettyUtils.releaseNettyLock(); + } + } } } diff --git a/instrumentation-security/netty-4.0.0/src/main/java/security/io/netty400/utils/NettyUtils.java b/instrumentation-security/netty-4.0.0/src/main/java/security/io/netty400/utils/NettyUtils.java index b4e022fec..2c1e9a3ff 100644 --- a/instrumentation-security/netty-4.0.0/src/main/java/security/io/netty400/utils/NettyUtils.java +++ b/instrumentation-security/netty-4.0.0/src/main/java/security/io/netty400/utils/NettyUtils.java @@ -26,6 +26,7 @@ public class NettyUtils { public static String NR_SEC_CUSTOM_ATTRIB_NAME = "NETTY-4.8-REQ-BODY-TRACKER"; + public static String NR_SEC_NETTY_OPERATIONAL_LOCK = "NR_SEC_NETTY_OPERATIONAL_LOCK"; private static final String X_FORWARDED_FOR = "x-forwarded-for"; private static final String EMPTY = ""; public static final String WRITE_METHOD_NAME = "write"; @@ -58,7 +59,7 @@ public static void processSecurityRequest(ChannelHandlerContext ctx, Object msg, securityRequest.setProtocol(((HttpRequest) msg).getProtocolVersion().protocolName()); securityRequest.setContentType(securityRequest.getHeaders().get("content-type")); StackTraceElement[] stack = Thread.currentThread().getStackTrace(); - securityMetaData.getMetaData().setServiceTrace(Arrays.copyOfRange(stack, 1, stack.length)); + securityMetaData.getMetaData().setServiceTrace(Arrays.copyOfRange(stack, 2, stack.length)); securityRequest.setRequestParsed(true); } else if (msg instanceof HttpContent) { if (!(secMetaObj instanceof SecurityMetaData) || @@ -213,4 +214,35 @@ private static void processResponseHeaders(HttpResponse response, com.newrelic.a securityResponse.getHeaders().put(headerKey, headerValue); } } + + public static boolean isNettyLockAcquired() { + try { + return NewRelicSecurity.isHookProcessingActive() && + Boolean.TRUE.equals(NewRelicSecurity.getAgent().getSecurityMetaData().getCustomAttribute(getNrSecOperationalLockName(), Boolean.class)); + } catch (Throwable ignored) {} + return false; + } + + public static boolean acquireNettyLockIfPossible() { + try { + if (NewRelicSecurity.isHookProcessingActive() && + !isNettyLockAcquired()) { + NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(getNrSecOperationalLockName(), true); + return true; + } + } catch (Throwable ignored){} + return false; + } + + public static void releaseNettyLock() { + try { + if(NewRelicSecurity.isHookProcessingActive()) { + NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(getNrSecOperationalLockName(), null); + } + } catch (Throwable ignored){} + } + + private static String getNrSecOperationalLockName() { + return NR_SEC_NETTY_OPERATIONAL_LOCK + Thread.currentThread().getId(); + } } diff --git a/instrumentation-security/netty-4.0.0/src/test/java/com/nr/agent/security/instrumentation/netty400/NettyTest.java b/instrumentation-security/netty-4.0.0/src/test/java/com/nr/agent/security/instrumentation/netty400/NettyTest.java new file mode 100644 index 000000000..4ceabeb60 --- /dev/null +++ b/instrumentation-security/netty-4.0.0/src/test/java/com/nr/agent/security/instrumentation/netty400/NettyTest.java @@ -0,0 +1,181 @@ +package com.nr.agent.security.instrumentation.netty400; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.newrelic.agent.security.introspec.InstrumentationTestConfig; +import com.newrelic.agent.security.introspec.SecurityInstrumentationTestRunner; +import com.newrelic.agent.security.introspec.SecurityIntrospector; +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.HttpRequest; +import com.newrelic.api.agent.security.schema.HttpResponse; +import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; +import com.newrelic.api.agent.security.schema.operation.RXSSOperation; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelInboundHandlerAdapter; + +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.DefaultHttpContent; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponseEncoder; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import org.junit.Assert; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; +import security.io.netty400.utils.NettyUtils; + +import java.util.List; + +@RunWith(SecurityInstrumentationTestRunner.class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@InstrumentationTestConfig(includePrefixes = {"security.io.netty400"}) +public class NettyTest { + @Test + public void testChannelRead() throws JsonProcessingException { + channelRead(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("Operations detected", operations.size() == 0); + HttpRequest request = introspector.getSecurityMetaData().getRequest(); + Assert.assertEquals("Invalid protocol", "HTTP", request.getProtocol()); + Assert.assertNotNull("No URL", request.getUrl()); + Assert.assertEquals("Invalid content-type", "text/html", request.getContentType()); + Assert.assertEquals("Invalid headers", "text/html", request.getHeaders().get("content-type")); + Assert.assertEquals("Invalid response body", "read data", request.getBody().toString()); + } + + @Test + public void testWrite() throws JsonProcessingException { + write(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + RXSSOperation operation = (RXSSOperation) operations.get(0); + Assert.assertEquals("Invalid executed method name", NettyUtils.WRITE_METHOD_NAME, operation.getMethodName()); + Assert.assertEquals("Invalid event category", VulnerabilityCaseType.REFLECTED_XSS, operation.getCaseType()); + + HttpResponse response = introspector.getSecurityMetaData().getResponse(); + Assert.assertEquals("Invalid content-type body", "text/html", response.getResponseContentType()); + Assert.assertEquals("Invalid content-type body", "text/html", response.getHeaders().get("content-type")); + Assert.assertEquals("Invalid response body", "write data", response.getResponseBody().toString()); + } + + @Test + public void testWriteAndFlush() throws JsonProcessingException { + writeAndFlush(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + RXSSOperation operation = (RXSSOperation) operations.get(0); + Assert.assertEquals("Invalid executed method name", NettyUtils.WRITE_METHOD_NAME, operation.getMethodName()); + Assert.assertEquals("Invalid event category", VulnerabilityCaseType.REFLECTED_XSS, operation.getCaseType()); + + HttpResponse response = introspector.getSecurityMetaData().getResponse(); + Assert.assertEquals("Invalid content-type body", "text/html", response.getResponseContentType()); + Assert.assertEquals("Invalid content-type body", "text/html", response.getHeaders().get("content-type")); + Assert.assertEquals("Invalid response body", "write flush data", response.getResponseBody().toString()); + } + + @Test + public void testWriteAndFlushPromise() throws JsonProcessingException { + writeAndFlushPromise(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + RXSSOperation operation = (RXSSOperation) operations.get(0); + Assert.assertEquals("Invalid executed method name", NettyUtils.WRITE_METHOD_NAME, operation.getMethodName()); + Assert.assertEquals("Invalid event category", VulnerabilityCaseType.REFLECTED_XSS, operation.getCaseType()); + + HttpResponse response = introspector.getSecurityMetaData().getResponse(); + Assert.assertEquals("Invalid content-type body", "text/html", response.getResponseContentType()); + Assert.assertEquals("Invalid content-type body", "text/html", response.getHeaders().get("content-type")); + Assert.assertEquals("Invalid response body", "write flush promise data", response.getResponseBody().toString()); + } + + @Test + public void testEncode() { + encode(); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + + RXSSOperation operation = (RXSSOperation) operations.get(0); + Assert.assertEquals("Invalid executed method name", NettyUtils.WRITE_METHOD_NAME, operation.getMethodName()); + Assert.assertEquals("Invalid event category", VulnerabilityCaseType.REFLECTED_XSS, operation.getCaseType()); + + HttpResponse response = introspector.getSecurityMetaData().getResponse(); + Assert.assertEquals("Invalid content-type body", "text/html", response.getResponseContentType()); + Assert.assertEquals("Invalid content-type body", "text/html", response.getHeaders().get("content-type")); + Assert.assertEquals("Invalid response body", "encode data", response.getResponseBody().toString()); + } + + @Trace(dispatcher = true) + private void channelRead() { + EmbeddedChannel channel = new EmbeddedChannel(new ChannelInboundHandlerAdapter()); + FullHttpRequest httpRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_0, HttpMethod.GET, "/test"); + httpRequest.headers().add("content-type", "text/html"); + DefaultHttpContent httpContent = new DefaultHttpContent(Unpooled.wrappedBuffer("read data".getBytes())); + channel.writeInbound(httpRequest, httpContent); + channel.read(); + } + + @Trace(dispatcher = true) + private void write() { + EmbeddedChannel channel = new EmbeddedChannel(new ChannelOutboundHandlerAdapter()); + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_0, HttpResponseStatus.ACCEPTED); + response.headers().add("content-type","text/html"); + response.content().writeBytes("write data".getBytes()); + + channel.write(response); + channel.flush(); + } + + @Trace(dispatcher = true) + private void writeAndFlush() { + EmbeddedChannel channel = new EmbeddedChannel(new ChannelOutboundHandlerAdapter()); + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_0, HttpResponseStatus.ACCEPTED); + response.headers().add("content-type","text/html"); + response.content().writeBytes("write flush data".getBytes()); + + channel.writeAndFlush(response); + } + + @Trace(dispatcher = true) + private void writeAndFlushPromise() { + EmbeddedChannel channel = new EmbeddedChannel(new ChannelOutboundHandlerAdapter()); + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_0, HttpResponseStatus.ACCEPTED); + response.headers().add("content-type","text/html"); + response.content().writeBytes("write flush promise data".getBytes()); + + channel.writeAndFlush(response, channel.newPromise()); + } + + @Trace(dispatcher = true) + private void encode() { + EmbeddedChannel channel = new EmbeddedChannel(new ChannelOutboundHandlerAdapter()); + channel.pipeline().addLast(new HttpResponseEncoder()); + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_0, HttpResponseStatus.ACCEPTED); + response.headers().add("content-type","text/html"); + response.content().writeBytes("encode data".getBytes()); + + channel.write(response); + channel.flush(); + } +} \ No newline at end of file diff --git a/instrumentation-security/okhttp-3.0.0/src/test/java/com/nr/agent/security/instrumentation/okhttp30/RealCallTest.java b/instrumentation-security/okhttp-3.0.0/src/test/java/com/nr/agent/security/instrumentation/okhttp30/RealCallTest.java index c79089e2f..6cd8428b7 100644 --- a/instrumentation-security/okhttp-3.0.0/src/test/java/com/nr/agent/security/instrumentation/okhttp30/RealCallTest.java +++ b/instrumentation-security/okhttp-3.0.0/src/test/java/com/nr/agent/security/instrumentation/okhttp30/RealCallTest.java @@ -6,6 +6,7 @@ import com.newrelic.agent.security.introspec.SecurityIntrospector; import com.newrelic.agent.security.introspec.internal.HttpServerRule; import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; import com.newrelic.api.agent.security.schema.AbstractOperation; import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; @@ -34,8 +35,7 @@ public void testExecute() throws Exception { String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); - introspector.setK2FuzzRequestId(headerValue); - introspector.setK2TracingData(headerValue); + setCSECHeaders(headerValue, introspector); try { httpClientExternal(server.getEndPoint().toString()); @@ -51,10 +51,24 @@ public void testExecute() throws Exception { Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue, headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", headerValue), headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + verifyHeaders(headerValue, headers); + } + + private void setCSECHeaders(String headerValue, SecurityIntrospector introspector) { + introspector.setK2FuzzRequestId(headerValue+"a"); + introspector.setK2ParentId(headerValue+"b"); + introspector.setK2TracingData(headerValue); + } + + private void verifyHeaders(String headerValue, Map headers) { + Assert.assertTrue(String.format("Missing K2 header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue+"a", headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); + Assert.assertTrue(String.format("Missing K2 header: %s", GenericHelper.CSEC_PARENT_ID), headers.containsKey(GenericHelper.CSEC_PARENT_ID)); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", GenericHelper.CSEC_PARENT_ID), headerValue+"b", headers.get(GenericHelper.CSEC_PARENT_ID)); + Assert.assertTrue(String.format("Missing K2 header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", + headerValue), headers.get( + ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); } @Trace(dispatcher = true) diff --git a/instrumentation-security/okhttp-3.5.0/src/test/java/com/nr/agent/security/instrumentation/okhttp35/RealCallTest.java b/instrumentation-security/okhttp-3.5.0/src/test/java/com/nr/agent/security/instrumentation/okhttp35/RealCallTest.java index 69ca834d2..72f20c1e2 100644 --- a/instrumentation-security/okhttp-3.5.0/src/test/java/com/nr/agent/security/instrumentation/okhttp35/RealCallTest.java +++ b/instrumentation-security/okhttp-3.5.0/src/test/java/com/nr/agent/security/instrumentation/okhttp35/RealCallTest.java @@ -6,6 +6,7 @@ import com.newrelic.agent.security.introspec.SecurityIntrospector; import com.newrelic.agent.security.introspec.internal.HttpServerRule; import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; import com.newrelic.api.agent.security.schema.AbstractOperation; import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; @@ -35,8 +36,7 @@ public void testExecute() throws Exception { String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); - introspector.setK2FuzzRequestId(headerValue); - introspector.setK2TracingData(headerValue); + setCSECHeaders(headerValue, introspector); try { httpClientExternal(server.getEndPoint().toString()); @@ -52,10 +52,24 @@ public void testExecute() throws Exception { Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue, headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", headerValue), headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + verifyHeaders(headerValue, headers); + } + + private void setCSECHeaders(String headerValue, SecurityIntrospector introspector) { + introspector.setK2FuzzRequestId(headerValue+"a"); + introspector.setK2ParentId(headerValue+"b"); + introspector.setK2TracingData(headerValue); + } + + private void verifyHeaders(String headerValue, Map headers) { + Assert.assertTrue(String.format("Missing K2 header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue+"a", headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); + Assert.assertTrue(String.format("Missing K2 header: %s", GenericHelper.CSEC_PARENT_ID), headers.containsKey(GenericHelper.CSEC_PARENT_ID)); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", GenericHelper.CSEC_PARENT_ID), headerValue+"b", headers.get(GenericHelper.CSEC_PARENT_ID)); + Assert.assertTrue(String.format("Missing K2 header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", + headerValue), headers.get( + ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); } @Trace(dispatcher = true) diff --git a/instrumentation-security/okhttp-4.0.0/src/test/java/com/nr/agent/security/instrumentation/okhttp40/ExchangeCodecTest.java b/instrumentation-security/okhttp-4.0.0/src/test/java/com/nr/agent/security/instrumentation/okhttp40/ExchangeCodecTest.java index d78cbfeff..6c0313289 100644 --- a/instrumentation-security/okhttp-4.0.0/src/test/java/com/nr/agent/security/instrumentation/okhttp40/ExchangeCodecTest.java +++ b/instrumentation-security/okhttp-4.0.0/src/test/java/com/nr/agent/security/instrumentation/okhttp40/ExchangeCodecTest.java @@ -6,6 +6,7 @@ import com.newrelic.agent.security.introspec.SecurityIntrospector; import com.newrelic.agent.security.introspec.internal.HttpServerRule; import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; import com.newrelic.api.agent.security.schema.AbstractOperation; import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; @@ -35,8 +36,7 @@ public void testExecute() throws Exception { String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); - introspector.setK2FuzzRequestId(headerValue); - introspector.setK2TracingData(headerValue); + setCSECHeaders(headerValue, introspector); httpClientExternal(server.getEndPoint().toString()); @@ -48,12 +48,25 @@ public void testExecute() throws Exception { Assert.assertEquals("Invalid executed parameters.", server.getEndPoint().toString(), operation.getArg()); Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed method name.", "execute", operation.getMethodName()); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue, headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", headerValue), headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + verifyHeaders(headerValue, headers); } + private void setCSECHeaders(String headerValue, SecurityIntrospector introspector) { + introspector.setK2FuzzRequestId(headerValue+"a"); + introspector.setK2ParentId(headerValue+"b"); + introspector.setK2TracingData(headerValue); + } + + private void verifyHeaders(String headerValue, Map headers) { + Assert.assertTrue(String.format("Missing K2 header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue+"a", headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); + Assert.assertTrue(String.format("Missing K2 header: %s", GenericHelper.CSEC_PARENT_ID), headers.containsKey(GenericHelper.CSEC_PARENT_ID)); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", GenericHelper.CSEC_PARENT_ID), headerValue+"b", headers.get(GenericHelper.CSEC_PARENT_ID)); + Assert.assertTrue(String.format("Missing K2 header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", + headerValue), headers.get( + ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + } @Trace(dispatcher = true) private void httpClientExternal(String host) throws IOException { diff --git a/instrumentation-security/r2dbc-oracle/build.gradle b/instrumentation-security/r2dbc-oracle/build.gradle index 3db046097..38339021c 100644 --- a/instrumentation-security/r2dbc-oracle/build.gradle +++ b/instrumentation-security/r2dbc-oracle/build.gradle @@ -10,7 +10,7 @@ jar { } verifyInstrumentation { - passesOnly 'com.oracle.database.r2dbc:oracle-r2dbc:[0.0.0,1.1.2)' + passesOnly 'com.oracle.database.r2dbc:oracle-r2dbc:[0.0.0,)' } //java { diff --git a/instrumentation-security/servlet-2.4/src/main/java/javax/servlet/FilterChain_Instrumentation.java b/instrumentation-security/servlet-2.4/src/main/java/javax/servlet/FilterChain_Instrumentation.java index d24551168..5cf38430a 100644 --- a/instrumentation-security/servlet-2.4/src/main/java/javax/servlet/FilterChain_Instrumentation.java +++ b/instrumentation-security/servlet-2.4/src/main/java/javax/servlet/FilterChain_Instrumentation.java @@ -15,6 +15,7 @@ import javax.servlet.http.HttpServletRequest; import java.io.IOException; +import java.util.Arrays; @Weave(type = MatchType.Interface, originalName = "javax.servlet.FilterChain") public abstract class FilterChain_Instrumentation { @@ -77,7 +78,9 @@ private void preprocessSecurityHook(ServletRequest request, ServletResponse resp } securityRequest.setContentType(httpServletRequest.getContentType()); - securityAgentMetaData.setServiceTrace(Thread.currentThread().getStackTrace()); + + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + securityMetaData.getMetaData().setServiceTrace(Arrays.copyOfRange(trace, 1, trace.length)); securityRequest.setRequestParsed(true); } catch (Throwable ignored){} } diff --git a/instrumentation-security/servlet-2.4/src/main/java/javax/servlet/Filter_Instrumentation.java b/instrumentation-security/servlet-2.4/src/main/java/javax/servlet/Filter_Instrumentation.java index 97cf78d6e..f1c30eff8 100644 --- a/instrumentation-security/servlet-2.4/src/main/java/javax/servlet/Filter_Instrumentation.java +++ b/instrumentation-security/servlet-2.4/src/main/java/javax/servlet/Filter_Instrumentation.java @@ -15,6 +15,7 @@ import javax.servlet.http.HttpServletRequest; import java.io.IOException; +import java.util.Arrays; @Weave(type = MatchType.Interface, originalName = "javax.servlet.Filter") public abstract class Filter_Instrumentation { @@ -79,7 +80,8 @@ private void preprocessSecurityHook(ServletRequest request, ServletResponse resp } securityRequest.setContentType(httpServletRequest.getContentType()); - securityAgentMetaData.setServiceTrace(Thread.currentThread().getStackTrace()); + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + securityMetaData.getMetaData().setServiceTrace(Arrays.copyOfRange(trace, 1, trace.length)); securityRequest.setRequestParsed(true); } catch (Throwable ignored){} } diff --git a/instrumentation-security/servlet-2.4/src/main/java/javax/servlet/Servlet_Instrumentation.java b/instrumentation-security/servlet-2.4/src/main/java/javax/servlet/Servlet_Instrumentation.java index 33bc5e052..b79752342 100644 --- a/instrumentation-security/servlet-2.4/src/main/java/javax/servlet/Servlet_Instrumentation.java +++ b/instrumentation-security/servlet-2.4/src/main/java/javax/servlet/Servlet_Instrumentation.java @@ -21,6 +21,7 @@ import com.newrelic.agent.security.instrumentation.servlet24.HttpServletHelper; import javax.servlet.http.HttpServletRequest; +import java.util.Arrays; @Weave(type = MatchType.Interface, originalName = "javax.servlet.Servlet") public abstract class Servlet_Instrumentation { @@ -83,7 +84,9 @@ private void preprocessSecurityHook(ServletRequest_Instrumentation request, Serv } securityRequest.setContentType(httpServletRequest.getContentType()); - securityAgentMetaData.setServiceTrace(Thread.currentThread().getStackTrace()); + + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + securityMetaData.getMetaData().setServiceTrace(Arrays.copyOfRange(trace, 1, trace.length)); securityRequest.setRequestParsed(true); } catch (Throwable ignored){} } diff --git a/instrumentation-security/servlet-2.4/src/main/java/javax/servlet/annotation/WebServlet_Instrumentation.java b/instrumentation-security/servlet-2.4/src/main/java/javax/servlet/annotation/WebServlet_Instrumentation.java deleted file mode 100644 index 893b73043..000000000 --- a/instrumentation-security/servlet-2.4/src/main/java/javax/servlet/annotation/WebServlet_Instrumentation.java +++ /dev/null @@ -1,16 +0,0 @@ -package javax.servlet.annotation; - -import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; -import com.newrelic.api.agent.weaver.MatchType; -import com.newrelic.api.agent.weaver.WeaveIntoAllMethods; -import com.newrelic.api.agent.weaver.WeaveWithAnnotation; - -@WeaveWithAnnotation(annotationClasses = {"javax.servlet.annotation.WebServlet"}, - type = MatchType.ExactClass) -public class WebServlet_Instrumentation { - @WeaveIntoAllMethods - private static void preprocessSecurityHook() { - ServletHelper.registerUserLevelCode("servlet-annotation"); - } - -} diff --git a/instrumentation-security/servlet-2.4/src/main/java/javax/servlet/http/HttpServletResponse_Instrumentation.java b/instrumentation-security/servlet-2.4/src/main/java/javax/servlet/http/HttpServletResponse_Instrumentation.java index 09600118e..1d55fc03f 100644 --- a/instrumentation-security/servlet-2.4/src/main/java/javax/servlet/http/HttpServletResponse_Instrumentation.java +++ b/instrumentation-security/servlet-2.4/src/main/java/javax/servlet/http/HttpServletResponse_Instrumentation.java @@ -6,6 +6,7 @@ import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; import com.newrelic.api.agent.security.schema.AbstractOperation; import com.newrelic.api.agent.security.schema.SecurityMetaData; +import com.newrelic.api.agent.security.schema.StringUtils; import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityException; import com.newrelic.api.agent.security.schema.operation.SecureCookieOperation; import com.newrelic.api.agent.weaver.MatchType; @@ -43,7 +44,15 @@ private AbstractOperation preprocessSecurityHook(Cookie cookie, String className return null; } - SecureCookieOperation operation = new SecureCookieOperation(Boolean.toString(cookie.getSecure()), className, methodName); + boolean isSecure = cookie.getSecure(); + boolean isHttpOnly = cookie.isHttpOnly(); + boolean sameSiteStrict = true; + if(NewRelicSecurity.getAgent().getServerInfo("SAME_SITE_COOKIES") != null){ + sameSiteStrict = StringUtils.equalsIgnoreCase(NewRelicSecurity.getAgent().getServerInfo("SAME_SITE_COOKIES"), "Strict"); + } else if(StringUtils.containsIgnoreCase(cookie.getValue(), "SameSite")) { + sameSiteStrict = StringUtils.containsIgnoreCase(cookie.getValue(), "SameSite=Strict"); + } + SecureCookieOperation operation = new SecureCookieOperation(Boolean.toString(isSecure), isSecure, isHttpOnly, sameSiteStrict, cookie.getValue(), className, methodName); operation.setLowSeverityHook(true); NewRelicSecurity.getAgent().registerOperation(operation); diff --git a/instrumentation-security/servlet-5.0/src/main/java/jakarta/servlet/FilterChain_Instrumentation.java b/instrumentation-security/servlet-5.0/src/main/java/jakarta/servlet/FilterChain_Instrumentation.java index 6f7e54a6e..933aa9a44 100644 --- a/instrumentation-security/servlet-5.0/src/main/java/jakarta/servlet/FilterChain_Instrumentation.java +++ b/instrumentation-security/servlet-5.0/src/main/java/jakarta/servlet/FilterChain_Instrumentation.java @@ -15,6 +15,7 @@ import jakarta.servlet.http.HttpServletRequest; import java.io.IOException; +import java.util.Arrays; @Weave(type = MatchType.Interface, originalName = "jakarta.servlet.FilterChain") public abstract class FilterChain_Instrumentation { @@ -77,7 +78,8 @@ private void preprocessSecurityHook(ServletRequest request, ServletResponse resp } securityRequest.setContentType(httpServletRequest.getContentType()); - securityAgentMetaData.setServiceTrace(Thread.currentThread().getStackTrace()); + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + securityMetaData.getMetaData().setServiceTrace(Arrays.copyOfRange(trace, 1, trace.length)); securityRequest.setRequestParsed(true); } catch (Throwable ignored){} } diff --git a/instrumentation-security/servlet-5.0/src/main/java/jakarta/servlet/Filter_Instrumentation.java b/instrumentation-security/servlet-5.0/src/main/java/jakarta/servlet/Filter_Instrumentation.java index 43e2be592..734be223e 100644 --- a/instrumentation-security/servlet-5.0/src/main/java/jakarta/servlet/Filter_Instrumentation.java +++ b/instrumentation-security/servlet-5.0/src/main/java/jakarta/servlet/Filter_Instrumentation.java @@ -15,6 +15,7 @@ import jakarta.servlet.http.HttpServletRequest; import java.io.IOException; +import java.util.Arrays; @Weave(type = MatchType.Interface, originalName = "jakarta.servlet.Filter") public abstract class Filter_Instrumentation { @@ -78,7 +79,9 @@ private void preprocessSecurityHook(ServletRequest request, ServletResponse resp } securityRequest.setContentType(httpServletRequest.getContentType()); - securityAgentMetaData.setServiceTrace(Thread.currentThread().getStackTrace()); + + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + securityMetaData.getMetaData().setServiceTrace(Arrays.copyOfRange(trace, 1, trace.length)); securityRequest.setRequestParsed(true); } catch (Throwable ignored){} } diff --git a/instrumentation-security/servlet-5.0/src/main/java/jakarta/servlet/Servlet_Instrumentation.java b/instrumentation-security/servlet-5.0/src/main/java/jakarta/servlet/Servlet_Instrumentation.java index 2b6acc230..bef8d08b9 100644 --- a/instrumentation-security/servlet-5.0/src/main/java/jakarta/servlet/Servlet_Instrumentation.java +++ b/instrumentation-security/servlet-5.0/src/main/java/jakarta/servlet/Servlet_Instrumentation.java @@ -21,6 +21,7 @@ import com.newrelic.agent.security.instrumentation.servlet5.HttpServletHelper; import jakarta.servlet.http.HttpServletRequest; +import java.util.Arrays; @Weave(type = MatchType.Interface, originalName = "jakarta.servlet.Servlet") public abstract class Servlet_Instrumentation { @@ -82,7 +83,9 @@ private void preprocessSecurityHook(ServletRequest_Instrumentation request, Serv securityRequest.setUrl(securityRequest.getUrl() + HttpServletHelper.QUESTION_MARK + queryString); } securityRequest.setContentType(httpServletRequest.getContentType()); - securityAgentMetaData.setServiceTrace(Thread.currentThread().getStackTrace()); + + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + securityMetaData.getMetaData().setServiceTrace(Arrays.copyOfRange(trace, 1, trace.length)); securityRequest.setRequestParsed(true); } catch (Throwable ignored){} } diff --git a/instrumentation-security/servlet-5.0/src/main/java/jakarta/servlet/annotation/WebServlet_Instrumentation.java b/instrumentation-security/servlet-5.0/src/main/java/jakarta/servlet/annotation/WebServlet_Instrumentation.java deleted file mode 100644 index 3d01a8960..000000000 --- a/instrumentation-security/servlet-5.0/src/main/java/jakarta/servlet/annotation/WebServlet_Instrumentation.java +++ /dev/null @@ -1,16 +0,0 @@ -package jakarta.servlet.annotation; - -import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; -import com.newrelic.api.agent.weaver.MatchType; -import com.newrelic.api.agent.weaver.WeaveIntoAllMethods; -import com.newrelic.api.agent.weaver.WeaveWithAnnotation; - -@WeaveWithAnnotation(annotationClasses = {"jakarta.servlet.annotation.WebServlet"}, - type = MatchType.ExactClass) -public class WebServlet_Instrumentation { - @WeaveIntoAllMethods - private static void preprocessSecurityHook() { - ServletHelper.registerUserLevelCode("servlet-annotation"); - } - -} diff --git a/instrumentation-security/servlet-5.0/src/main/java/jakarta/servlet/http/HttpServletResponse_Instrumentation.java b/instrumentation-security/servlet-5.0/src/main/java/jakarta/servlet/http/HttpServletResponse_Instrumentation.java index b90017e32..bc27af381 100644 --- a/instrumentation-security/servlet-5.0/src/main/java/jakarta/servlet/http/HttpServletResponse_Instrumentation.java +++ b/instrumentation-security/servlet-5.0/src/main/java/jakarta/servlet/http/HttpServletResponse_Instrumentation.java @@ -6,6 +6,7 @@ import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; import com.newrelic.api.agent.security.schema.AbstractOperation; import com.newrelic.api.agent.security.schema.SecurityMetaData; +import com.newrelic.api.agent.security.schema.StringUtils; import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityException; import com.newrelic.api.agent.security.schema.operation.SecureCookieOperation; import com.newrelic.api.agent.weaver.MatchType; @@ -43,7 +44,15 @@ private AbstractOperation preprocessSecurityHook(Cookie cookie, String className return null; } - SecureCookieOperation operation = new SecureCookieOperation(Boolean.toString(cookie.getSecure()), className, methodName); + boolean isSecure = cookie.getSecure(); + boolean isHttpOnly = cookie.isHttpOnly(); + boolean sameSiteStrict = true; + if(NewRelicSecurity.getAgent().getServerInfo("SAME_SITE_COOKIES") != null){ + sameSiteStrict = StringUtils.equalsIgnoreCase(NewRelicSecurity.getAgent().getServerInfo("SAME_SITE_COOKIES"), "Strict"); + } else if(StringUtils.containsIgnoreCase(cookie.getValue(), "SameSite")) { + sameSiteStrict = StringUtils.containsIgnoreCase(cookie.getValue(), "SameSite=Strict"); + } + SecureCookieOperation operation = new SecureCookieOperation(Boolean.toString(isSecure), isSecure, isHttpOnly, sameSiteStrict, cookie.getValue(), className, methodName); operation.setLowSeverityHook(true); NewRelicSecurity.getAgent().registerOperation(operation); diff --git a/instrumentation-security/servlet-6.0/src/main/java/jakarta/servlet/FilterChain_Instrumentation.java b/instrumentation-security/servlet-6.0/src/main/java/jakarta/servlet/FilterChain_Instrumentation.java index 185e2d14c..da52c8943 100644 --- a/instrumentation-security/servlet-6.0/src/main/java/jakarta/servlet/FilterChain_Instrumentation.java +++ b/instrumentation-security/servlet-6.0/src/main/java/jakarta/servlet/FilterChain_Instrumentation.java @@ -15,6 +15,7 @@ import jakarta.servlet.http.HttpServletRequest; import java.io.IOException; +import java.util.Arrays; @Weave(type = MatchType.Interface, originalName = "jakarta.servlet.FilterChain") public abstract class FilterChain_Instrumentation { @@ -77,7 +78,9 @@ private void preprocessSecurityHook(ServletRequest request, ServletResponse resp } securityRequest.setContentType(httpServletRequest.getContentType()); - securityAgentMetaData.setServiceTrace(Thread.currentThread().getStackTrace()); + + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + securityMetaData.getMetaData().setServiceTrace(Arrays.copyOfRange(trace, 1, trace.length)); securityRequest.setRequestParsed(true); } catch (Throwable ignored){} } diff --git a/instrumentation-security/servlet-6.0/src/main/java/jakarta/servlet/Filter_Instrumentation.java b/instrumentation-security/servlet-6.0/src/main/java/jakarta/servlet/Filter_Instrumentation.java index 8e5c6fda7..8c44191c5 100644 --- a/instrumentation-security/servlet-6.0/src/main/java/jakarta/servlet/Filter_Instrumentation.java +++ b/instrumentation-security/servlet-6.0/src/main/java/jakarta/servlet/Filter_Instrumentation.java @@ -15,6 +15,7 @@ import jakarta.servlet.http.HttpServletRequest; import java.io.IOException; +import java.util.Arrays; @Weave(type = MatchType.Interface, originalName = "jakarta.servlet.Filter") public abstract class Filter_Instrumentation { @@ -78,7 +79,9 @@ private void preprocessSecurityHook(ServletRequest request, ServletResponse resp } securityRequest.setContentType(httpServletRequest.getContentType()); - securityAgentMetaData.setServiceTrace(Thread.currentThread().getStackTrace()); + + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + securityMetaData.getMetaData().setServiceTrace(Arrays.copyOfRange(trace, 1, trace.length)); securityRequest.setRequestParsed(true); } catch (Throwable ignored){} } diff --git a/instrumentation-security/servlet-6.0/src/main/java/jakarta/servlet/Servlet_Instrumentation.java b/instrumentation-security/servlet-6.0/src/main/java/jakarta/servlet/Servlet_Instrumentation.java index 562fbf982..afd63a160 100644 --- a/instrumentation-security/servlet-6.0/src/main/java/jakarta/servlet/Servlet_Instrumentation.java +++ b/instrumentation-security/servlet-6.0/src/main/java/jakarta/servlet/Servlet_Instrumentation.java @@ -21,6 +21,7 @@ import com.newrelic.agent.security.instrumentation.servlet6.HttpServletHelper; import jakarta.servlet.http.HttpServletRequest; +import java.util.Arrays; @Weave(type = MatchType.Interface, originalName = "jakarta.servlet.Servlet") public abstract class Servlet_Instrumentation { @@ -82,7 +83,9 @@ private void preprocessSecurityHook(ServletRequest_Instrumentation request, Serv securityRequest.setUrl(securityRequest.getUrl() + HttpServletHelper.QUESTION_MARK + queryString); } securityRequest.setContentType(httpServletRequest.getContentType()); - securityAgentMetaData.setServiceTrace(Thread.currentThread().getStackTrace()); + + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + securityMetaData.getMetaData().setServiceTrace(Arrays.copyOfRange(trace, 1, trace.length)); securityRequest.setRequestParsed(true); } catch (Throwable ignored){} } diff --git a/instrumentation-security/servlet-6.0/src/main/java/jakarta/servlet/annotation/WebServlet_Instrumentation.java b/instrumentation-security/servlet-6.0/src/main/java/jakarta/servlet/annotation/WebServlet_Instrumentation.java deleted file mode 100644 index 3d01a8960..000000000 --- a/instrumentation-security/servlet-6.0/src/main/java/jakarta/servlet/annotation/WebServlet_Instrumentation.java +++ /dev/null @@ -1,16 +0,0 @@ -package jakarta.servlet.annotation; - -import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; -import com.newrelic.api.agent.weaver.MatchType; -import com.newrelic.api.agent.weaver.WeaveIntoAllMethods; -import com.newrelic.api.agent.weaver.WeaveWithAnnotation; - -@WeaveWithAnnotation(annotationClasses = {"jakarta.servlet.annotation.WebServlet"}, - type = MatchType.ExactClass) -public class WebServlet_Instrumentation { - @WeaveIntoAllMethods - private static void preprocessSecurityHook() { - ServletHelper.registerUserLevelCode("servlet-annotation"); - } - -} diff --git a/instrumentation-security/servlet-6.0/src/main/java/jakarta/servlet/http/HttpServletResponse_Instrumentation.java b/instrumentation-security/servlet-6.0/src/main/java/jakarta/servlet/http/HttpServletResponse_Instrumentation.java index b90017e32..564c36e96 100644 --- a/instrumentation-security/servlet-6.0/src/main/java/jakarta/servlet/http/HttpServletResponse_Instrumentation.java +++ b/instrumentation-security/servlet-6.0/src/main/java/jakarta/servlet/http/HttpServletResponse_Instrumentation.java @@ -6,6 +6,7 @@ import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; import com.newrelic.api.agent.security.schema.AbstractOperation; import com.newrelic.api.agent.security.schema.SecurityMetaData; +import com.newrelic.api.agent.security.schema.StringUtils; import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityException; import com.newrelic.api.agent.security.schema.operation.SecureCookieOperation; import com.newrelic.api.agent.weaver.MatchType; @@ -43,7 +44,16 @@ private AbstractOperation preprocessSecurityHook(Cookie cookie, String className return null; } - SecureCookieOperation operation = new SecureCookieOperation(Boolean.toString(cookie.getSecure()), className, methodName); + //"https".equals(securityMetaData.getRequest().getProtocol()) || + boolean isSecure = cookie.getSecure(); + boolean isHttpOnly = cookie.isHttpOnly(); + boolean sameSiteStrict = true; + if(NewRelicSecurity.getAgent().getServerInfo("SAME_SITE_COOKIES") != null){ + sameSiteStrict = StringUtils.equalsIgnoreCase(NewRelicSecurity.getAgent().getServerInfo("SAME_SITE_COOKIES"), "Strict"); + } else if(StringUtils.containsIgnoreCase(cookie.getValue(), "SameSite")) { + sameSiteStrict = StringUtils.containsIgnoreCase(cookie.getValue(), "SameSite=Strict"); + } + SecureCookieOperation operation = new SecureCookieOperation(Boolean.toString(isSecure ), isSecure, isHttpOnly, sameSiteStrict, cookie.getValue(), className, methodName); operation.setLowSeverityHook(true); NewRelicSecurity.getAgent().registerOperation(operation); diff --git a/instrumentation-security/spring-data-redis/build.gradle b/instrumentation-security/spring-data-redis/build.gradle new file mode 100644 index 000000000..651e8c3bd --- /dev/null +++ b/instrumentation-security/spring-data-redis/build.gradle @@ -0,0 +1,24 @@ +dependencies { + implementation(project(":newrelic-security-api")) + implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}") + implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}") + implementation("org.springframework.data:spring-data-redis:1.1.0.RELEASE") +} + +jar { + manifest { + attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.spring-data-redis' + } +} + + +verifyInstrumentation { + passesOnly 'org.springframework.data:spring-data-redis:[1.1.0.RELEASE,)' + exclude 'org.springframework.data:spring-data-redis:[,1.1.0.RELEASE)' + excludeRegex '.*(RC|M).*' +} + +site { + title 'spring-data-redis 1.1.0' + type 'Framework' +} diff --git a/instrumentation-security/spring-data-redis/src/main/java/org/springframework/data/redis/core/AbstractOperations_Instrumentation.java b/instrumentation-security/spring-data-redis/src/main/java/org/springframework/data/redis/core/AbstractOperations_Instrumentation.java new file mode 100644 index 000000000..2c7b52813 --- /dev/null +++ b/instrumentation-security/spring-data-redis/src/main/java/org/springframework/data/redis/core/AbstractOperations_Instrumentation.java @@ -0,0 +1,66 @@ +package org.springframework.data.redis.core; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; + +@Weave(type = MatchType.BaseClass, originalName = "org.springframework.data.redis.core.AbstractOperations") +abstract class AbstractOperations_Instrumentation { + + byte[] rawHashKey(HK hashKey) { + byte[] returnValue = null; + returnValue = Weaver.callOriginal(); + + createRedisArgumentEntry(returnValue.hashCode(), hashKey); + + return returnValue; + } + + byte[] rawHashValue(HV value) { + byte[] returnValue = null; + returnValue = Weaver.callOriginal(); + + createRedisArgumentEntry(returnValue.hashCode(), value); + + return returnValue; + } + + byte[] rawKey(Object key) { + System.out.println("raw key : "+key); + byte[] returnValue = null; + returnValue = Weaver.callOriginal(); + + createRedisArgumentEntry(returnValue.hashCode(), key); + + return returnValue; + } + + byte[] rawString(String key) { + byte[] returnValue = null; + returnValue = Weaver.callOriginal(); + + createRedisArgumentEntry(returnValue.hashCode(), key); + + return returnValue; + } + + byte[] rawValue(Object value) { + System.out.println("raw value : "+value); + byte[] returnValue = null; + returnValue = Weaver.callOriginal(); + + createRedisArgumentEntry(returnValue.hashCode(), value); + + return returnValue; + } + + private void createRedisArgumentEntry(int hashCode, Object entry) { + if (!NewRelicSecurity.isHookProcessingActive() || + NewRelicSecurity.getAgent().getSecurityMetaData().getRequest().isEmpty()){ + return; + } + NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(GenericHelper.NR_SEC_CUSTOM_SPRING_REDIS_ATTR + hashCode, entry); + } +} diff --git a/instrumentation-security/spymemcached-2.12.0/build.gradle b/instrumentation-security/spymemcached-2.12.0/build.gradle new file mode 100644 index 000000000..6a2719160 --- /dev/null +++ b/instrumentation-security/spymemcached-2.12.0/build.gradle @@ -0,0 +1,23 @@ +dependencies { + implementation("net.spy:spymemcached:2.12.0") + implementation(project(":newrelic-security-api")) + implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}") + implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}") + testImplementation("com.github.mwarc:embedded-memcached-spring:0.1.4") +} + +jar { + manifest { + attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.spymemcached-2.12.0' + attributes 'Implementation-Vendor': 'com.newrelic', 'Implementation-Version': 1.0 + } +} + +verifyInstrumentation { + passesOnly 'net.spy:spymemcached:[2.9.0,)' +} + +site { + title 'Spymemcached' + type 'Messaging' +} \ No newline at end of file diff --git a/instrumentation-security/spymemcached-2.12.0/src/main/java/com/newrelic/agent/security/instrumentation/spy/memcached/MemcachedHelper.java b/instrumentation-security/spymemcached-2.12.0/src/main/java/com/newrelic/agent/security/instrumentation/spy/memcached/MemcachedHelper.java new file mode 100644 index 000000000..76d5e076a --- /dev/null +++ b/instrumentation-security/spymemcached-2.12.0/src/main/java/com/newrelic/agent/security/instrumentation/spy/memcached/MemcachedHelper.java @@ -0,0 +1,50 @@ +package com.newrelic.agent.security.instrumentation.spy.memcached; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityException; +import com.newrelic.api.agent.security.schema.operation.MemcachedOperation; +import net.spy.memcached.ops.CASOperation; +import net.spy.memcached.ops.ConcatenationOperation; +import net.spy.memcached.ops.Operation; +import net.spy.memcached.ops.StoreOperation; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.util.Arrays; + +public class MemcachedHelper { + public static final String NR_SEC_CUSTOM_ATTRIB_NAME = "MEMCACHED_OPERATION_LOCK_"; + public static final String WRITE = "write"; + public static final String UPDATE = "update"; + public static final String METHOD_ASYNC_STORE = "asyncStore"; + public static final String METHOD_ASYNC_CAT = "asyncCat"; + public static final String METHOD_ASYNC_CAS = "asyncCAS"; + + public static AbstractOperation preprocessSecurityHook(String command, String type, String key, Object val, String klass, String method) { + try { + if (!NewRelicSecurity.isHookProcessingActive() || NewRelicSecurity.getAgent().getSecurityMetaData().getRequest().isEmpty()){ + return null; + } + MemcachedOperation operation = new MemcachedOperation(command, Arrays.asList(key, val), type, klass, method); + NewRelicSecurity.getAgent().registerOperation(operation); + return operation; + } catch (Throwable e) { + if (e instanceof NewRelicSecurityException) { + throw e; + } + } + return null; + } + + public static void registerExitOperation(boolean isProcessingAllowed, AbstractOperation operation) { + try { + if (operation == null || !isProcessingAllowed || !NewRelicSecurity.isHookProcessingActive() || + NewRelicSecurity.getAgent().getSecurityMetaData().getRequest().isEmpty()) { + return; + } + NewRelicSecurity.getAgent().registerExitEvent(operation); + } catch (Throwable ignored){} + } +} diff --git a/instrumentation-security/spymemcached-2.12.0/src/main/java/net/spy/memcached/MemcachedClient_Instrumentation.java b/instrumentation-security/spymemcached-2.12.0/src/main/java/net/spy/memcached/MemcachedClient_Instrumentation.java new file mode 100644 index 000000000..66658d078 --- /dev/null +++ b/instrumentation-security/spymemcached-2.12.0/src/main/java/net/spy/memcached/MemcachedClient_Instrumentation.java @@ -0,0 +1,76 @@ +package net.spy.memcached; + +import com.newrelic.agent.security.instrumentation.spy.memcached.MemcachedHelper; +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityException; +import com.newrelic.api.agent.security.schema.operation.MemcachedOperation; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import net.spy.memcached.internal.OperationFuture; +import net.spy.memcached.ops.ConcatenationType; +import net.spy.memcached.ops.Operation; +import net.spy.memcached.ops.StoreType; +import net.spy.memcached.transcoders.Transcoder; + +@Weave(originalName = "net.spy.memcached.MemcachedClient") +public class MemcachedClient_Instrumentation { + + private OperationFuture asyncStore(StoreType storeType, + String key, int exp, T value, Transcoder tc) { + boolean isLockAcquired = GenericHelper.acquireLockIfPossible(MemcachedHelper.NR_SEC_CUSTOM_ATTRIB_NAME, value.hashCode()); + AbstractOperation operation = null; + if (isLockAcquired) { + operation = MemcachedHelper.preprocessSecurityHook(storeType.name(), MemcachedHelper.WRITE, key, value, this.getClass().getName(), MemcachedHelper.METHOD_ASYNC_STORE); + } + OperationFuture returnValue = null; + try { + returnValue = Weaver.callOriginal(); + } finally { + if (isLockAcquired) { + GenericHelper.releaseLock(MemcachedHelper.NR_SEC_CUSTOM_ATTRIB_NAME, value.hashCode()); + } + } + MemcachedHelper.registerExitOperation(isLockAcquired, operation); + return returnValue; + } + + private OperationFuture asyncCat(ConcatenationType catType, + long cas, String key, T value, Transcoder tc) { + boolean isLockAcquired = GenericHelper.acquireLockIfPossible(MemcachedHelper.NR_SEC_CUSTOM_ATTRIB_NAME, value.hashCode()); + AbstractOperation operation = null; + if (isLockAcquired) { + operation = MemcachedHelper.preprocessSecurityHook(catType.name(), MemcachedHelper.UPDATE, key, value, this.getClass().getName(), MemcachedHelper.METHOD_ASYNC_CAT); + } + OperationFuture returnValue = null; + try { + returnValue = Weaver.callOriginal(); + } finally { + if (isLockAcquired) { + GenericHelper.releaseLock(MemcachedHelper.NR_SEC_CUSTOM_ATTRIB_NAME, value.hashCode()); + } + } + MemcachedHelper.registerExitOperation(isLockAcquired, operation); + return returnValue; + } + + public OperationFuture + asyncCAS(String key, long casId, int exp, T value, Transcoder tc) { + boolean isLockAcquired = GenericHelper.acquireLockIfPossible(MemcachedHelper.NR_SEC_CUSTOM_ATTRIB_NAME, value.hashCode()); + AbstractOperation operation = null; + if (isLockAcquired) { + operation = MemcachedHelper.preprocessSecurityHook(StoreType.set.name(), MemcachedHelper.WRITE, key, value, this.getClass().getName(), MemcachedHelper.METHOD_ASYNC_CAS); + } + OperationFuture returnValue = null; + try { + returnValue = Weaver.callOriginal(); + } finally { + if (isLockAcquired) { + GenericHelper.releaseLock(MemcachedHelper.NR_SEC_CUSTOM_ATTRIB_NAME, value.hashCode()); + } + } + MemcachedHelper.registerExitOperation(isLockAcquired, operation); + return returnValue; + } +} diff --git a/instrumentation-security/spymemcached-2.12.0/src/test/java/com/nr/agent/security/instrumentation/memcached/test/MemcachedTest.java b/instrumentation-security/spymemcached-2.12.0/src/test/java/com/nr/agent/security/instrumentation/memcached/test/MemcachedTest.java new file mode 100644 index 000000000..22f798b3e --- /dev/null +++ b/instrumentation-security/spymemcached-2.12.0/src/test/java/com/nr/agent/security/instrumentation/memcached/test/MemcachedTest.java @@ -0,0 +1,183 @@ +package com.nr.agent.security.instrumentation.memcached.test; + +import com.github.mwarc.embeddedmemcached.JMemcachedServer; +import com.newrelic.agent.security.introspec.InstrumentationTestConfig; +import com.newrelic.agent.security.introspec.SecurityInstrumentationTestRunner; +import com.newrelic.agent.security.introspec.SecurityIntrospector; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; +import com.newrelic.api.agent.security.schema.operation.MemcachedOperation; +import net.spy.memcached.MemcachedClient; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.util.Arrays; +import java.util.List; + +@RunWith(SecurityInstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = { "net.spy.memcached" }) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class MemcachedTest { + + public static final String WRITE = "write"; + public static final String UPDATE = "update"; + private static MemcachedClient memcachedClient; + private final String key = "someKey"; + private final String value = "value"; + private final int expirationInSeconds = 1800; + private final long casID = 1; + private static JMemcachedServer server; + private static int port = 0; + private static int getRandomPort() { + try (ServerSocket socket = new ServerSocket(port)){ + port = socket.getLocalPort(); + } catch (IOException e) { + throw new RuntimeException("Unable to allocate ephemeral port "+ port); + } + return port; + } + + @BeforeClass + public static void setup() throws IOException { + port = getRandomPort(); + server = new JMemcachedServer(); + server.start("127.0.0.1", port); + memcachedClient = new MemcachedClient(new InetSocketAddress("127.0.0.1", port)); + } + + @AfterClass + public static void stop() { + memcachedClient.flush(); + memcachedClient.shutdown(); + server.clean(); + } + @Test + public void testSet() { + memcachedClient.set(key, expirationInSeconds, value); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertEquals("No operations detected.", 1, operations.size()); + MemcachedOperation operation = (MemcachedOperation) operations.get(0); + + verifier(operation, Arrays.asList(key, value), WRITE, "asyncStore"); + } + + @Test + public void testAdd() { + memcachedClient.add(key, expirationInSeconds, value); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertEquals("No operations detected.", 1, operations.size()); + MemcachedOperation operation = (MemcachedOperation) operations.get(0); + + verifier(operation, Arrays.asList(key, value), WRITE, "asyncStore"); + } + @Test + public void testReplace() { + memcachedClient.replace(key, expirationInSeconds, value); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertEquals("No operations detected.", 1, operations.size()); + MemcachedOperation operation = (MemcachedOperation) operations.get(0); + + verifier(operation, Arrays.asList(key, value), WRITE, "asyncStore"); + } + @Test + public void testAppend() { + memcachedClient.append(key, value); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertEquals("No operations detected.", 1, operations.size()); + MemcachedOperation operation = (MemcachedOperation) operations.get(0); + + verifier(operation, Arrays.asList(key, value), UPDATE, "asyncCat"); + } + @Test + public void testPrepend() { + memcachedClient.prepend(key, value); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertEquals("No operations detected.", 1, operations.size()); + MemcachedOperation operation = (MemcachedOperation) operations.get(0); + + verifier(operation, Arrays.asList(key, value), UPDATE, "asyncCat"); + } + @Test + public void testPrepend1() { + memcachedClient.prepend(casID, key, value); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertEquals("No operations detected.", 1, operations.size()); + MemcachedOperation operation = (MemcachedOperation) operations.get(0); + + verifier(operation, Arrays.asList(key, value), UPDATE, "asyncCat"); + } + @Test + public void testCas() { + memcachedClient.cas(key, casID, value); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertEquals("No operations detected.", 1, operations.size()); + MemcachedOperation operation = (MemcachedOperation) operations.get(0); + + verifier(operation, Arrays.asList(key, value), WRITE, "asyncCAS"); + } + @Test + public void testCas1() { + memcachedClient.cas(key, casID, expirationInSeconds, value); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertEquals("No operations detected.", 1, operations.size()); + MemcachedOperation operation = (MemcachedOperation) operations.get(0); + + verifier(operation, Arrays.asList(key, value), WRITE, "asyncCAS"); + } + + @Test + public void testAsyncCAS() { + memcachedClient.asyncCAS(key, casID, value); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertEquals("No operations detected.", 1, operations.size()); + MemcachedOperation operation = (MemcachedOperation) operations.get(0); + + verifier(operation, Arrays.asList(key, value), WRITE, "asyncCAS"); + } + @Test + public void testAsyncCAS1() { + memcachedClient.asyncCAS(key, casID, expirationInSeconds, value); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertEquals("No operations detected.", 1, operations.size()); + MemcachedOperation operation = (MemcachedOperation) operations.get(0); + + verifier(operation, Arrays.asList(key, value), WRITE, "asyncCAS"); + } + + private void verifier(MemcachedOperation operation, List args, String type, String method) { + Assert.assertEquals("Incorrect executed parameters.", args, operation.getArguments()); + Assert.assertEquals("Incorrect event case type.", VulnerabilityCaseType.CACHING_DATA_STORE, operation.getCaseType()); + Assert.assertEquals("Incorrect event category.", MemcachedOperation.MEMCACHED, operation.getCategory()); + Assert.assertEquals("Incorrect event category.", type, operation.getType()); + Assert.assertEquals("Incorrect executed class-name.", memcachedClient.getClass().getName(), operation.getClassName()); + Assert.assertEquals("Incorrect executed method-name.", method, operation.getMethodName()); + } +} diff --git a/instrumentation-security/sun-net-httpserver/build.gradle b/instrumentation-security/sun-net-httpserver/build.gradle new file mode 100644 index 000000000..89575877f --- /dev/null +++ b/instrumentation-security/sun-net-httpserver/build.gradle @@ -0,0 +1,9 @@ +dependencies { + implementation(project(":newrelic-security-api")) + implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}") + implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}") +} + +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.sun-net-httpserver' } +} \ No newline at end of file diff --git a/instrumentation-security/sun-net-httpserver/src/main/java/com/sun/net/httpserver/Filter_Instrumentation.java b/instrumentation-security/sun-net-httpserver/src/main/java/com/sun/net/httpserver/Filter_Instrumentation.java new file mode 100644 index 000000000..1a59567eb --- /dev/null +++ b/instrumentation-security/sun-net-httpserver/src/main/java/com/sun/net/httpserver/Filter_Instrumentation.java @@ -0,0 +1,108 @@ +package com.sun.net.httpserver; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.instrumentation.helpers.LowSeverityHelper; +import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; +import com.newrelic.api.agent.security.schema.AgentMetaData; +import com.newrelic.api.agent.security.schema.HttpRequest; +import com.newrelic.api.agent.security.schema.SecurityMetaData; +import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityException; +import com.newrelic.api.agent.security.schema.operation.RXSSOperation; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; + +import java.io.IOException; +import java.util.Arrays; + +@Weave(originalName = "com.sun.net.httpserver.Filter", type = MatchType.BaseClass) +public class Filter_Instrumentation { + public void doFilter (HttpExchange exchange, Filter.Chain chain) throws IOException { + boolean isServletLockAcquired = acquireServletLockIfPossible(); + + if (isServletLockAcquired){ + preprocessSecurityHook(exchange); + } + try{ + Weaver.callOriginal(); + } finally { + if (isServletLockAcquired){ + releaseServletLock(); + } + } + if (isServletLockAcquired){ + postProcessSecurityHook(exchange); + } + } + + private void preprocessSecurityHook(HttpExchange exchange) { + try { + if (!NewRelicSecurity.isHookProcessingActive()) { + return; + } + SecurityMetaData securityMetaData = NewRelicSecurity.getAgent().getSecurityMetaData(); + + HttpRequest securityRequest = securityMetaData.getRequest(); + if (securityRequest.isRequestParsed()) { + return; + } + + AgentMetaData securityAgentMetaData = securityMetaData.getMetaData(); + securityRequest.setMethod(exchange.getRequestMethod()); + securityRequest.setClientIP(exchange.getRemoteAddress().getAddress().getHostAddress()); + securityRequest.setServerPort(exchange.getLocalAddress().getPort()); + + if (securityRequest.getClientIP() != null && !securityRequest.getClientIP().trim().isEmpty()) { + securityAgentMetaData.getIps().add(securityRequest.getClientIP()); + securityRequest.setClientPort(String.valueOf(exchange.getRemoteAddress().getPort())); + } + + HttpServerHelper.processHttpRequestHeaders(exchange.getRequestHeaders(), securityRequest); + securityMetaData.setTracingHeaderValue(HttpServerHelper.getTraceHeader(securityRequest.getHeaders())); + securityRequest.setProtocol(HttpServerHelper.getProtocol(exchange)); + securityRequest.setUrl(String.valueOf(exchange.getRequestURI())); + + String queryString = exchange.getRequestURI().getQuery(); + if (queryString != null && !queryString.trim().isEmpty()) { + securityRequest.setUrl(securityRequest.getUrl() + HttpServerHelper.QUESTION_MARK + queryString); + } + + securityRequest.setContentType(HttpServerHelper.getContentType(exchange.getRequestHeaders())); + ServletHelper.registerUserLevelCode("sun-net-http-server"); + securityRequest.setRequestParsed(true); + } catch (Throwable ignored){} + } + private void postProcessSecurityHook(HttpExchange exchange) { + try { + if (!NewRelicSecurity.isHookProcessingActive()) { + return; + } + //Add request URI hash to low severity event filter + LowSeverityHelper.addRrequestUriToEventFilter(NewRelicSecurity.getAgent().getSecurityMetaData().getRequest()); + + RXSSOperation rxssOperation = new RXSSOperation(NewRelicSecurity.getAgent().getSecurityMetaData().getRequest(), + NewRelicSecurity.getAgent().getSecurityMetaData().getResponse(), + this.getClass().getName(), HttpServerHelper.HANDLE_METHOD_NAME); + NewRelicSecurity.getAgent().registerOperation(rxssOperation); + ServletHelper.tmpFileCleanUp(NewRelicSecurity.getAgent().getSecurityMetaData().getFuzzRequestIdentifier().getTempFiles()); + } catch (Throwable e) { + if(e instanceof NewRelicSecurityException){ + e.printStackTrace(); + throw e; + } + } + } + + private boolean acquireServletLockIfPossible() { + try { + return HttpServerHelper.acquireServletLockIfPossible(); + } catch (Throwable ignored) {} + return false; + } + private void releaseServletLock() { + try { + HttpServerHelper.releaseServletLock(); + } catch (Throwable ignored) {} + } + +} diff --git a/instrumentation-security/sun-net-httpserver/src/main/java/com/sun/net/httpserver/HttpExchange_Instrumentation.java b/instrumentation-security/sun-net-httpserver/src/main/java/com/sun/net/httpserver/HttpExchange_Instrumentation.java new file mode 100644 index 000000000..a4b457d61 --- /dev/null +++ b/instrumentation-security/sun-net-httpserver/src/main/java/com/sun/net/httpserver/HttpExchange_Instrumentation.java @@ -0,0 +1,31 @@ +package com.sun.net.httpserver; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import java.io.InputStream; + +import static com.sun.net.httpserver.HttpServerHelper.SUN_NET_READER_OPERATION_LOCK; + +@Weave(type = MatchType.BaseClass, originalName = "com.sun.net.httpserver.HttpExchange") +public class HttpExchange_Instrumentation { + + public InputStream getRequestBody () { + boolean isLockAcquired = false; + InputStream stream; + try { + isLockAcquired = GenericHelper.acquireLockIfPossible(SUN_NET_READER_OPERATION_LOCK); + stream = Weaver.callOriginal(); + if (isLockAcquired && NewRelicSecurity.isHookProcessingActive() && stream != null) { + HttpServerHelper.registerInputStreamHashIfNeeded(stream.hashCode()); + } + } finally { + if(isLockAcquired) { + GenericHelper.releaseLock(SUN_NET_READER_OPERATION_LOCK); + } + } + return stream; + } +} diff --git a/instrumentation-security/sun-net-httpserver/src/main/java/com/sun/net/httpserver/HttpHandler_Instrumentation.java b/instrumentation-security/sun-net-httpserver/src/main/java/com/sun/net/httpserver/HttpHandler_Instrumentation.java new file mode 100644 index 000000000..2b0f40137 --- /dev/null +++ b/instrumentation-security/sun-net-httpserver/src/main/java/com/sun/net/httpserver/HttpHandler_Instrumentation.java @@ -0,0 +1,107 @@ +package com.sun.net.httpserver; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.instrumentation.helpers.LowSeverityHelper; +import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; +import com.newrelic.api.agent.security.schema.AgentMetaData; +import com.newrelic.api.agent.security.schema.HttpRequest; +import com.newrelic.api.agent.security.schema.SecurityMetaData; +import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityException; +import com.newrelic.api.agent.security.schema.operation.RXSSOperation; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; + +import java.io.IOException; + +@Weave(originalName = "com.sun.net.httpserver.HttpHandler", type = MatchType.Interface) +public class HttpHandler_Instrumentation { + public void handle (HttpExchange exchange) throws IOException { + boolean isServletLockAcquired = acquireServletLockIfPossible(); + + if (isServletLockAcquired){ + preprocessSecurityHook(exchange); + } + try{ + Weaver.callOriginal(); + } finally { + if (isServletLockAcquired){ + releaseServletLock(); + } + } + if (isServletLockAcquired){ + postProcessSecurityHook(exchange); + } + } + + private void preprocessSecurityHook(HttpExchange exchange) { + try { + if (!NewRelicSecurity.isHookProcessingActive()) { + return; + } + SecurityMetaData securityMetaData = NewRelicSecurity.getAgent().getSecurityMetaData(); + + HttpRequest securityRequest = securityMetaData.getRequest(); + if (securityRequest.isRequestParsed()) { + return; + } + + AgentMetaData securityAgentMetaData = securityMetaData.getMetaData(); + securityRequest.setMethod(exchange.getRequestMethod()); + securityRequest.setClientIP(exchange.getRemoteAddress().getAddress().getHostAddress()); + securityRequest.setServerPort(exchange.getLocalAddress().getPort()); + + if (securityRequest.getClientIP() != null && !securityRequest.getClientIP().trim().isEmpty()) { + securityAgentMetaData.getIps().add(securityRequest.getClientIP()); + securityRequest.setClientPort(String.valueOf(exchange.getRemoteAddress().getPort())); + } + + HttpServerHelper.processHttpRequestHeaders(exchange.getRequestHeaders(), securityRequest); + securityMetaData.setTracingHeaderValue(HttpServerHelper.getTraceHeader(securityRequest.getHeaders())); + securityRequest.setProtocol(HttpServerHelper.getProtocol(exchange)); + securityRequest.setUrl(String.valueOf(exchange.getRequestURI())); + + String queryString = exchange.getRequestURI().getQuery(); + if (queryString != null && !queryString.trim().isEmpty()) { + securityRequest.setUrl(securityRequest.getUrl() + HttpServerHelper.QUESTION_MARK + queryString); + } + + securityRequest.setContentType(HttpServerHelper.getContentType(exchange.getRequestHeaders())); + + ServletHelper.registerUserLevelCode("sun-net-http-server"); + securityRequest.setRequestParsed(true); + } catch (Throwable ignored){} + } + private void postProcessSecurityHook(HttpExchange exchange) { + try { + if (!NewRelicSecurity.isHookProcessingActive()) { + return; + } + //Add request URI hash to low severity event filter + LowSeverityHelper.addRrequestUriToEventFilter(NewRelicSecurity.getAgent().getSecurityMetaData().getRequest()); + + RXSSOperation rxssOperation = new RXSSOperation(NewRelicSecurity.getAgent().getSecurityMetaData().getRequest(), + NewRelicSecurity.getAgent().getSecurityMetaData().getResponse(), + this.getClass().getName(), HttpServerHelper.HANDLE_METHOD_NAME); + NewRelicSecurity.getAgent().registerOperation(rxssOperation); + ServletHelper.tmpFileCleanUp(NewRelicSecurity.getAgent().getSecurityMetaData().getFuzzRequestIdentifier().getTempFiles()); + } catch (Throwable e) { + if(e instanceof NewRelicSecurityException){ + e.printStackTrace(); + throw e; + } + } + } + + private boolean acquireServletLockIfPossible() { + try { + return HttpServerHelper.acquireServletLockIfPossible(); + } catch (Throwable ignored) {} + return false; + } + private void releaseServletLock() { + try { + HttpServerHelper.releaseServletLock(); + } catch (Throwable ignored) {} + } +} diff --git a/instrumentation-security/sun-net-httpserver/src/main/java/com/sun/net/httpserver/HttpServerHelper.java b/instrumentation-security/sun-net-httpserver/src/main/java/com/sun/net/httpserver/HttpServerHelper.java new file mode 100644 index 000000000..ca8cd20bb --- /dev/null +++ b/instrumentation-security/sun-net-httpserver/src/main/java/com/sun/net/httpserver/HttpServerHelper.java @@ -0,0 +1,130 @@ +package com.sun.net.httpserver; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; +import com.newrelic.api.agent.security.schema.AgentMetaData; +import com.newrelic.api.agent.security.schema.HttpRequest; +import com.newrelic.api.agent.security.schema.policy.AgentPolicy; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class HttpServerHelper { + private static final String X_FORWARDED_FOR = "x-forwarded-for"; + private static final String NR_SEC_CUSTOM_ATTRIB_NAME = "HTTPSERVER_LOCK-"; + public static final String HANDLE_METHOD_NAME = "handle"; + private static final String EMPTY = ""; + private static final String CONTENT_TYPE = "Content-type"; + public static final String QUESTION_MARK = "?"; + public static final String HTTP_PROTOCOL = "http"; + public static final String HTTPS_PROTOCOL = "https"; + private static final String REQUEST_INPUTSTREAM_HASH = "REQUEST_INPUTSTREAM_HASH"; + public static final String SUN_NET_READER_OPERATION_LOCK = "SUN_NET_READER_OPERATION_LOCK-"; + + public static void processHttpRequestHeaders(Headers headers, HttpRequest securityRequest){ + for (String headerKey : headers.keySet()) { + boolean takeNextValue = false; + if (headerKey != null) { + headerKey = headerKey.toLowerCase(); + } + AgentPolicy agentPolicy = NewRelicSecurity.getAgent().getCurrentPolicy(); + AgentMetaData agentMetaData = NewRelicSecurity.getAgent().getSecurityMetaData().getMetaData(); + if (agentPolicy != null && agentPolicy.getProtectionMode().getEnabled() + && agentPolicy.getProtectionMode().getIpBlocking().getEnabled() + && agentPolicy.getProtectionMode().getIpBlocking().getIpDetectViaXFF() + && X_FORWARDED_FOR.equals(headerKey)) { + takeNextValue = true; + } else if (ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID.equals(headerKey)) { + // TODO: May think of removing this intermediate obj and directly create K2 Identifier. + NewRelicSecurity.getAgent().getSecurityMetaData() + .setFuzzRequestIdentifier(ServletHelper.parseFuzzRequestIdentifierHeader(headers.getFirst(headerKey))); + } else if(GenericHelper.CSEC_PARENT_ID.equals(headerKey)) { + NewRelicSecurity.getAgent().getSecurityMetaData() + .addCustomAttribute(GenericHelper.CSEC_PARENT_ID, headers.getFirst(headerKey)); + } + String headerFullValue = EMPTY; + for (String headerValue : headers.get(headerKey)) { + if (headerValue != null && !headerValue.trim().isEmpty()) { + if (takeNextValue) { + agentMetaData.setClientDetectedFromXFF(true); + securityRequest.setClientIP(headerValue); + agentMetaData.getIps() + .add(securityRequest.getClientIP()); + securityRequest.setClientPort(EMPTY); + takeNextValue = false; + } + if (headerFullValue.trim().isEmpty()) { + headerFullValue = headerValue; + } else { + headerFullValue = String.join(";", headerFullValue, headerValue); + } + } + } + securityRequest.getHeaders().put(headerKey, headerFullValue); + } + } + public static String getContentType(Headers headers){ + String data = EMPTY; + if (headers.containsKey(CONTENT_TYPE)) { + data = headers.getFirst(CONTENT_TYPE); + } + return data; + } + public static String getTraceHeader(Map headers) { + String data = EMPTY; + if (headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER) || headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())) { + data = headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER); + if (data == null || data.trim().isEmpty()) { + data = headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase()); + } + } + return data; + } + + public static void registerInputStreamHashIfNeeded(int inputStreamHash){ + try { + Set hashSet = NewRelicSecurity.getAgent().getSecurityMetaData().getCustomAttribute(REQUEST_INPUTSTREAM_HASH, Set.class); + if(hashSet == null){ + hashSet = new HashSet<>(); + NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(REQUEST_INPUTSTREAM_HASH, hashSet); + } + hashSet.add(inputStreamHash); + } catch (Throwable ignored) {} + } + + public static boolean acquireServletLockIfPossible() { + try { + if (NewRelicSecurity.isHookProcessingActive() && !isServletLockAcquired()) { + NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(getNrSecCustomAttribName(), true); + return true; + } + } catch (Throwable ignored) {} + return false; + } + public static void releaseServletLock() { + try { + if(NewRelicSecurity.isHookProcessingActive()) { + NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(getNrSecCustomAttribName(), null); + } + } catch (Throwable ignored){} + } + private static String getNrSecCustomAttribName() { + return NR_SEC_CUSTOM_ATTRIB_NAME + Thread.currentThread().getId(); + } + public static boolean isServletLockAcquired() { + try { + return NewRelicSecurity.isHookProcessingActive() && + Boolean.TRUE.equals(NewRelicSecurity.getAgent().getSecurityMetaData().getCustomAttribute(getNrSecCustomAttribName(), Boolean.class)); + } catch (Throwable ignored) {} + return false; + } + + public static String getProtocol(HttpExchange exchange){ + if (exchange instanceof HttpsExchange){ + return HTTPS_PROTOCOL; + } + return HTTP_PROTOCOL; + } +} diff --git a/instrumentation-security/sun-net-httpserver/src/test/java/com/nr/agent/security/instrumentation/httpServer/test/HttpServerTest.java b/instrumentation-security/sun-net-httpserver/src/test/java/com/nr/agent/security/instrumentation/httpServer/test/HttpServerTest.java new file mode 100644 index 000000000..74b263c61 --- /dev/null +++ b/instrumentation-security/sun-net-httpserver/src/test/java/com/nr/agent/security/instrumentation/httpServer/test/HttpServerTest.java @@ -0,0 +1,170 @@ +package com.nr.agent.security.instrumentation.httpServer.test; + +import com.newrelic.agent.security.introspec.InstrumentationTestConfig; +import com.newrelic.agent.security.introspec.SecurityInstrumentationTestRunner; +import com.newrelic.agent.security.introspec.SecurityIntrospector; +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; +import com.newrelic.api.agent.security.schema.operation.RXSSOperation; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@RunWith(SecurityInstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = { "com.sun.net.httpserver"}) +public class HttpServerTest { + @ClassRule + public static Httpserver server = new Httpserver(); + + @Test + public void testHandle() throws URISyntaxException, IOException, InterruptedException { + handle(); + Thread.sleep(100); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + Assert.assertEquals("Extra operations detected", 1, operations.size()); + + RXSSOperation targetOperation = (RXSSOperation) operations.get(0); + Assert.assertNotNull("No target operation detected", targetOperation); + Assert.assertEquals("Wrong case-type detected", VulnerabilityCaseType.REFLECTED_XSS, targetOperation.getCaseType()); + Assert.assertEquals("Wrong client IP detected", "127.0.0.1", targetOperation.getRequest().getClientIP()); + Assert.assertEquals("Wrong Protocol detected", "http", targetOperation.getRequest().getProtocol()); + + Assert.assertEquals("Wrong port detected", server.getEndPoint().getPort(), targetOperation.getRequest().getServerPort()); + Assert.assertEquals("Wrong method name detected", "handle", targetOperation.getMethodName()); + Assert.assertEquals("Wrong Content-type detected", "text/plain", targetOperation.getRequest().getContentType()); + } + @Test + public void testHandle1() throws URISyntaxException, IOException, InterruptedException { + String headerValue = handle1(); + Thread.sleep(100); + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + Assert.assertEquals("Extra operations detected", 1, operations.size()); + + RXSSOperation targetOperation = (RXSSOperation) operations.get(0); + Assert.assertNotNull("No target operation detected", targetOperation); + Assert.assertEquals("Wrong case-type detected", VulnerabilityCaseType.REFLECTED_XSS, targetOperation.getCaseType()); + Assert.assertEquals("Wrong client IP detected", "127.0.0.1", targetOperation.getRequest().getClientIP()); + Assert.assertEquals("Wrong Protocol detected", "http", targetOperation.getRequest().getProtocol()); + + Assert.assertEquals("Wrong port detected", server.getEndPoint().getPort(), targetOperation.getRequest().getServerPort()); + Assert.assertEquals("Wrong method name detected", "handle", targetOperation.getMethodName()); + Assert.assertEquals("Wrong Content-type detected", "text/plain", targetOperation.getRequest().getContentType()); + + Map headers = targetOperation.getRequest().getHeaders(); + Assert.assertTrue( + String.format("Missing header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), + headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID) + ); + Assert.assertEquals( + String.format("Invalid header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), + headerValue, + headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID) + ); + Assert.assertTrue( + String.format("Missing header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), + headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase()) + ); + Assert.assertEquals( + String.format("Invalid header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), + headerValue, + headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase()) + ); + Assert.assertTrue( + String.format("Missing K2 header: %s", GenericHelper.CSEC_PARENT_ID), + headers.containsKey(GenericHelper.CSEC_PARENT_ID) + ); + Assert.assertEquals( + String.format("Invalid K2 header value for: %s", GenericHelper.CSEC_PARENT_ID), + headerValue, headers.get(GenericHelper.CSEC_PARENT_ID) + ); + } + @Test + public void testHandle2() throws URISyntaxException, IOException, InterruptedException { + int expectedHash = handle2(); + Thread.sleep(100); + + SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); + List operations = introspector.getOperations(); + Assert.assertTrue("No operations detected", operations.size() > 0); + Assert.assertEquals("Extra operations detected", 1, operations.size()); + + RXSSOperation targetOperation = (RXSSOperation) operations.get(0); + Assert.assertNotNull("No target operation detected", targetOperation); + Assert.assertEquals("Wrong case-type detected", VulnerabilityCaseType.REFLECTED_XSS, targetOperation.getCaseType()); + Assert.assertEquals("Wrong client IP detected", "127.0.0.1", targetOperation.getRequest().getClientIP()); + Assert.assertEquals("Wrong Protocol detected", "http", targetOperation.getRequest().getProtocol()); + + Assert.assertEquals("Wrong port detected", server.getEndPoint().getPort(), targetOperation.getRequest().getServerPort()); + Assert.assertEquals("Wrong method name detected", "handle", targetOperation.getMethodName()); + Assert.assertEquals("Wrong Content-type detected", "text/plain", targetOperation.getRequest().getContentType()); + + Assert.assertNotNull("No hashcode detected", introspector.getRequestInStreamHash()); + Assert.assertEquals("Wrong hashcode detected", Collections.singleton(expectedHash), introspector.getRequestInStreamHash()); + } + + @Trace(dispatcher = true) + private void handle() throws URISyntaxException, IOException { + URL url = server.getEndPoint().toURL(); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestProperty("content-type", "text/plain; charset=utf-8"); + conn.connect(); + conn.getResponseCode(); + + } + + private String handle1() throws URISyntaxException, IOException { + String headerValue = String.valueOf(UUID.randomUUID()); + + URL url = server.getEndPoint().toURL(); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestProperty("content-type", "text/plain; charset=utf-8"); + conn.setRequestProperty("content-type", "text/plain; charset=utf-8"); + conn.setRequestMethod("GET"); + conn.setRequestProperty(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID, headerValue); + conn.setRequestProperty(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER, headerValue); + conn.setRequestProperty(GenericHelper.CSEC_PARENT_ID, headerValue); + conn.connect(); + conn.getResponseCode(); + return headerValue; + } + + @Trace(dispatcher = true) + private int handle2() throws URISyntaxException, IOException { + URL url = server.getEndPoint().toURL(); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestProperty("content-type", "text/plain; charset=utf-8"); + conn.setRequestMethod("POST"); + + conn.connect(); + conn.getResponseCode(); + + BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream())); + int hashCode = 0; + String code; + + if((code = (br.readLine())) != null){ + hashCode = Integer.parseInt(code); + } + br.close(); + return hashCode; + } +} diff --git a/instrumentation-security/sun-net-httpserver/src/test/java/com/nr/agent/security/instrumentation/httpServer/test/Httpserver.java b/instrumentation-security/sun-net-httpserver/src/test/java/com/nr/agent/security/instrumentation/httpServer/test/Httpserver.java new file mode 100644 index 000000000..9af1c767c --- /dev/null +++ b/instrumentation-security/sun-net-httpserver/src/test/java/com/nr/agent/security/instrumentation/httpServer/test/Httpserver.java @@ -0,0 +1,70 @@ +package com.nr.agent.security.instrumentation.httpServer.test; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import org.junit.rules.ExternalResource; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.URI; +import java.net.URISyntaxException; + +public class Httpserver extends ExternalResource { + private HttpServer server; + private static final int PORT = getRandomPort(); + @Override + protected void before() throws Throwable { + startServer(); + } + + @Override + protected void after() { + stop(); + } + + public void startServer() throws IOException { + server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), PORT), 0); + server.createContext("/", new Handler()); + server.setExecutor(null); + server.start(); + } + + public void stop() { + server.stop(0); + } + + private static int getRandomPort() { + int port = 0; + try (ServerSocket socket = new ServerSocket(0)){ + port = socket.getLocalPort(); + } catch (IOException e) { + throw new RuntimeException("Unable to allocate ephemeral port "+ port); + } + return port; + } + + public URI getEndPoint() throws URISyntaxException { + return new URI("http://localhost:" + PORT); + } + static class Handler implements HttpHandler { + + @Override + public void handle(HttpExchange exchange) throws IOException { + OutputStream os = exchange.getResponseBody(); + String response; + if(exchange.getRequestMethod().equals("POST")){ + response = String.valueOf(exchange.getRequestBody().hashCode()); + } else { + response = "Hello, World!\n"; + } + exchange.sendResponseHeaders(200, response.length()); + os.write(response.getBytes()); + os.flush(); + os.close(); + } + } +} diff --git a/instrumentation-security/tomcat-7/build.gradle b/instrumentation-security/tomcat-7/build.gradle new file mode 100644 index 000000000..6515f024c --- /dev/null +++ b/instrumentation-security/tomcat-7/build.gradle @@ -0,0 +1,21 @@ +dependencies { + implementation(project(":newrelic-security-api")) + implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}") + implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}") + implementation("org.apache.tomcat.embed:tomcat-embed-core:8.0.1") + +} + +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.tomcat-7' } +} + +verifyInstrumentation { + passesOnly 'org.apache.tomcat.embed:tomcat-embed-core:[8.0.1,)' + excludeRegex '.*-(b|gfa|beta|RC)[0-9]*' +} + +site { + title 'Tomcat' + type 'Appserver' +} \ No newline at end of file diff --git a/instrumentation-security/tomcat-7/src/main/java/com/newrelic/agent/security/instrumentation/tomcat7/Container_Instrumentation.java b/instrumentation-security/tomcat-7/src/main/java/com/newrelic/agent/security/instrumentation/tomcat7/Container_Instrumentation.java new file mode 100644 index 000000000..8992bff7f --- /dev/null +++ b/instrumentation-security/tomcat-7/src/main/java/com/newrelic/agent/security/instrumentation/tomcat7/Container_Instrumentation.java @@ -0,0 +1,21 @@ +package com.newrelic.agent.security.instrumentation.tomcat7; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; + +import java.io.File; + +@Weave(type = MatchType.Interface, originalName = "org.apache.catalina.Container") +public abstract class Container_Instrumentation { + + public File getCatalinaBase() { + File returnValue = Weaver.callOriginal(); + if(returnValue != null) { + NewRelicSecurity.getAgent().setServerInfo("SERVER_BASE_DIRECTORY", returnValue.getAbsolutePath()); + } + return returnValue; + } + +} diff --git a/instrumentation-security/tomcat-7/src/main/java/com/newrelic/agent/security/instrumentation/tomcat7/StandardHost_Instrumentation.java b/instrumentation-security/tomcat-7/src/main/java/com/newrelic/agent/security/instrumentation/tomcat7/StandardHost_Instrumentation.java new file mode 100644 index 000000000..c366d662b --- /dev/null +++ b/instrumentation-security/tomcat-7/src/main/java/com/newrelic/agent/security/instrumentation/tomcat7/StandardHost_Instrumentation.java @@ -0,0 +1,19 @@ +package com.newrelic.agent.security.instrumentation.tomcat7; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; + +import java.io.File; + +@Weave(type = MatchType.ExactClass, originalName = "org.apache.catalina.core.StandardHost") +public class StandardHost_Instrumentation { + + public String getAppBase() { + String returnValue = Weaver.callOriginal(); + NewRelicSecurity.getAgent().setServerInfo("APPLICATION_DIRECTORY", returnValue); + return returnValue; + } + +} diff --git a/instrumentation-security/tomcat-8/build.gradle b/instrumentation-security/tomcat-8/build.gradle new file mode 100644 index 000000000..bac3cfd89 --- /dev/null +++ b/instrumentation-security/tomcat-8/build.gradle @@ -0,0 +1,22 @@ +dependencies { + implementation(project(":newrelic-security-api")) + implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}") + implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}") + implementation("org.apache.tomcat.embed:tomcat-embed-core:8.5.42") + +} + +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.tomcat-8' } +} + +verifyInstrumentation { + passesOnly 'org.apache.tomcat.embed:tomcat-embed-core:[8.5.42,8.5.97)' + passesOnly 'org.apache.tomcat.embed:tomcat-embed-core:[9.0.21,)' + excludeRegex '.*-(b|gfa|beta|RC)[0-9]*' +} + +site { + title 'Tomcat' + type 'Appserver' +} \ No newline at end of file diff --git a/instrumentation-security/tomcat-8/src/main/java/com/newrelic/agent/security/instrumentation/tomcat7/CookieProcessorBase_Instrumentation.java b/instrumentation-security/tomcat-8/src/main/java/com/newrelic/agent/security/instrumentation/tomcat7/CookieProcessorBase_Instrumentation.java new file mode 100644 index 000000000..5a3f9e02d --- /dev/null +++ b/instrumentation-security/tomcat-8/src/main/java/com/newrelic/agent/security/instrumentation/tomcat7/CookieProcessorBase_Instrumentation.java @@ -0,0 +1,15 @@ +package com.newrelic.agent.security.instrumentation.tomcat7; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; + +@Weave(type = MatchType.Interface, originalName = "org.apache.tomcat.util.http.CookieProcessorBase") +public abstract class CookieProcessorBase_Instrumentation { + + public void setSameSiteCookies(String sameSiteCookies) { + Weaver.callOriginal(); + NewRelicSecurity.getAgent().setServerInfo("SAME_SITE_COOKIES", sameSiteCookies); + } +} diff --git a/instrumentation-security/urlconnection/src/test/java/com/nr/agent/security/instrumentation/urlconnection/URLConnectionTest.java b/instrumentation-security/urlconnection/src/test/java/com/nr/agent/security/instrumentation/urlconnection/URLConnectionTest.java index a3b643dc1..abb52a53a 100644 --- a/instrumentation-security/urlconnection/src/test/java/com/nr/agent/security/instrumentation/urlconnection/URLConnectionTest.java +++ b/instrumentation-security/urlconnection/src/test/java/com/nr/agent/security/instrumentation/urlconnection/URLConnectionTest.java @@ -5,6 +5,7 @@ import com.newrelic.agent.security.introspec.SecurityIntrospector; import com.newrelic.agent.security.introspec.internal.HttpServerRule; import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; import com.newrelic.api.agent.security.schema.AbstractOperation; import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; @@ -41,8 +42,7 @@ public void testConnect() throws IOException { String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); - introspector.setK2FuzzRequestId(headerValue); - introspector.setK2TracingData(headerValue); + setCSECHeaders(headerValue, introspector); callConnect(endpoint); List operations = introspector.getOperations(); @@ -53,10 +53,7 @@ public void testConnect() throws IOException { Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed class name.", "sun.net.www.protocol.http.HttpURLConnection", operation.getClassName()); Assert.assertEquals("Invalid executed method name.", "connect", operation.getMethodName()); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue, headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;",headerValue), headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + verifyHeaders(headerValue, headers); } @@ -66,8 +63,7 @@ public void testConnect1() throws IOException { String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); - introspector.setK2FuzzRequestId(headerValue); - introspector.setK2TracingData(headerValue); + setCSECHeaders(headerValue, introspector); callConnect1(endpoint); List operations = introspector.getOperations(); @@ -78,10 +74,7 @@ public void testConnect1() throws IOException { Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed class name.", sun.net.www.protocol.http.HttpURLConnection.class.getName(), operation.getClassName()); Assert.assertEquals("Invalid executed method name.", "connect", operation.getMethodName()); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue, headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;",headerValue), headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + verifyHeaders(headerValue, headers); } @Test @@ -89,8 +82,7 @@ public void testGetInputStream() throws IOException { String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); - introspector.setK2FuzzRequestId(headerValue); - introspector.setK2TracingData(headerValue); + setCSECHeaders(headerValue, introspector); callGetInputStream(endpoint); List operations = introspector.getOperations(); @@ -101,10 +93,7 @@ public void testGetInputStream() throws IOException { Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed class name.", "sun.net.www.protocol.http.HttpURLConnection", operation.getClassName()); Assert.assertEquals("Invalid executed method name.", "getInputStream", operation.getMethodName()); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue, headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;",headerValue), headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + verifyHeaders(headerValue, headers); } @Test @@ -112,8 +101,7 @@ public void testGetInputStreamByGetContent() throws IOException { String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); - introspector.setK2FuzzRequestId(headerValue); - introspector.setK2TracingData(headerValue); + setCSECHeaders(headerValue, introspector); callGetInputStreamByGetContent(endpoint); List operations = introspector.getOperations(); @@ -124,10 +112,7 @@ public void testGetInputStreamByGetContent() throws IOException { Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed class name.", sun.net.www.protocol.http.HttpURLConnection.class.getName(), operation.getClassName()); Assert.assertEquals("Invalid executed method name.", "getInputStream", operation.getMethodName()); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue, headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;",headerValue), headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + verifyHeaders(headerValue, headers); } @Test @@ -135,8 +120,7 @@ public void testGetInputStreamByGetContent1() throws IOException { String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); - introspector.setK2FuzzRequestId(headerValue); - introspector.setK2TracingData(headerValue); + setCSECHeaders(headerValue, introspector); callGetInputStreamByGetContent1(endpoint); List operations = introspector.getOperations(); @@ -147,10 +131,7 @@ public void testGetInputStreamByGetContent1() throws IOException { Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed class name.", sun.net.www.protocol.http.HttpURLConnection.class.getName(), operation.getClassName()); Assert.assertEquals("Invalid executed method name.", "getInputStream", operation.getMethodName()); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue, headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;",headerValue), headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + verifyHeaders(headerValue, headers); } @Test @@ -158,8 +139,7 @@ public void testGetInputStreamByOpenStream() throws IOException { String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); - introspector.setK2FuzzRequestId(headerValue); - introspector.setK2TracingData(headerValue); + setCSECHeaders(headerValue, introspector); callGetInputStreamByOpenStream(endpoint); List operations = introspector.getOperations(); @@ -170,10 +150,7 @@ public void testGetInputStreamByOpenStream() throws IOException { Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed class name.", sun.net.www.protocol.http.HttpURLConnection.class.getName(), operation.getClassName()); Assert.assertEquals("Invalid executed method name.", "getInputStream", operation.getMethodName()); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue, headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;",headerValue), headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + verifyHeaders(headerValue, headers); } @Test @@ -181,8 +158,7 @@ public void testGetInputStreamByConGetContent() throws IOException { String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); - introspector.setK2FuzzRequestId(headerValue); - introspector.setK2TracingData(headerValue); + setCSECHeaders(headerValue, introspector); callGetInputStreamByConGetContent(endpoint); List operations = introspector.getOperations(); @@ -193,10 +169,7 @@ public void testGetInputStreamByConGetContent() throws IOException { Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed class name.", sun.net.www.protocol.http.HttpURLConnection.class.getName(), operation.getClassName()); Assert.assertEquals("Invalid executed method name.", "getInputStream", operation.getMethodName()); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue, headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;",headerValue), headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + verifyHeaders(headerValue, headers); } @Test @@ -204,8 +177,7 @@ public void testGetInputStreamByConGetContent1() throws IOException { String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); - introspector.setK2FuzzRequestId(headerValue); - introspector.setK2TracingData(headerValue); + setCSECHeaders(headerValue, introspector); callGetInputStreamByConGetContent1(endpoint); List operations = introspector.getOperations(); @@ -217,10 +189,7 @@ public void testGetInputStreamByConGetContent1() throws IOException { Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed class name.", sun.net.www.protocol.http.HttpURLConnection.class.getName(), operation.getClassName()); Assert.assertEquals("Invalid executed method name.", "getInputStream", operation.getMethodName()); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue, headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;",headerValue), headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + verifyHeaders(headerValue, headers); } @Test @@ -229,8 +198,7 @@ public void testGetOutputStream() throws IOException { String headerValue = String.valueOf(UUID.randomUUID()); SecurityIntrospector introspector = SecurityInstrumentationTestRunner.getIntrospector(); - introspector.setK2FuzzRequestId(headerValue); - introspector.setK2TracingData(headerValue); + setCSECHeaders(headerValue, introspector); callGetOutputStream(endpoint); List operations = introspector.getOperations(); @@ -242,10 +210,7 @@ public void testGetOutputStream() throws IOException { Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType()); Assert.assertEquals("Invalid executed class name.", "sun.net.www.protocol.http.HttpURLConnection", operation.getClassName()); Assert.assertEquals("Invalid executed method name.", "getOutputStream", operation.getMethodName()); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue, headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); - Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); - Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;",headerValue), headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + verifyHeaders(headerValue, headers); } @Trace(dispatcher = true) @@ -300,4 +265,21 @@ private void callGetOutputStream(String endpoint) throws IOException { output.write(1); } } + + private void setCSECHeaders(String headerValue, SecurityIntrospector introspector) { + introspector.setK2FuzzRequestId(headerValue+"a"); + introspector.setK2ParentId(headerValue+"b"); + introspector.setK2TracingData(headerValue); + } + + private void verifyHeaders(String headerValue, Map headers) { + Assert.assertTrue(String.format("Missing K2 header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue+"a", headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); + Assert.assertTrue(String.format("Missing K2 header: %s", GenericHelper.CSEC_PARENT_ID), headers.containsKey(GenericHelper.CSEC_PARENT_ID)); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", GenericHelper.CSEC_PARENT_ID), headerValue+"b", headers.get(GenericHelper.CSEC_PARENT_ID)); + Assert.assertTrue(String.format("Missing K2 header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + Assert.assertEquals(String.format("Invalid K2 header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", + headerValue), headers.get( + ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())); + } } diff --git a/newrelic-security-agent/build.gradle b/newrelic-security-agent/build.gradle index 5314eca31..9a55bc196 100644 --- a/newrelic-security-agent/build.gradle +++ b/newrelic-security-agent/build.gradle @@ -76,6 +76,7 @@ dependencies { shadowIntoJar 'com.github.erosb:everit-json-schema:1.14.2' shadowIntoJar 'net.openhft:zero-allocation-hashing:0.16' shadowIntoJar 'com.github.oshi:oshi-core:6.4.1' + shadowIntoJar 'com.google.code.gson:gson:2.10.1' shadowIntoJar "com.newrelic.agent.java:newrelic-api:${nrAPIVersion}" } diff --git a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/AgentConfig.java b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/AgentConfig.java index dd640a9e6..a6500dc27 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/AgentConfig.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/AgentConfig.java @@ -10,6 +10,7 @@ import com.newrelic.agent.security.intcodeagent.utils.CommonUtils; import com.newrelic.agent.security.util.IUtilConstants; import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.security.instrumentation.helpers.LowSeverityHelper; import org.apache.commons.io.FileUtils; import org.apache.commons.io.comparator.LastModifiedFileComparator; import org.apache.commons.io.filefilter.FileFilterUtils; @@ -52,6 +53,8 @@ public void instantiate(){ isNRSecurityEnabled = NewRelic.getAgent().getConfig().getValue(IUtilConstants.NR_SECURITY_ENABLED, false); // Set required Group groupName = applyRequiredGroup(); + // Enable low severity hooks + LowSeverityHelper.enableLowSeverityHooks(groupName); // Set required LogLevel logLevel = applyRequiredLogLevel(); diff --git a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/dispatcher/Dispatcher.java b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/dispatcher/Dispatcher.java index a6deb2a50..e55cd83b1 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/dispatcher/Dispatcher.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/dispatcher/Dispatcher.java @@ -1,5 +1,6 @@ package com.newrelic.agent.security.instrumentator.dispatcher; +import com.google.gson.Gson; import com.newrelic.agent.security.AgentInfo; import com.newrelic.agent.security.instrumentator.helper.DynamoDBRequestConverter; import com.newrelic.agent.security.instrumentator.utils.AgentUtils; @@ -11,8 +12,11 @@ import com.newrelic.agent.security.intcodeagent.models.javaagent.ExitEventBean; import com.newrelic.agent.security.intcodeagent.models.javaagent.JavaAgentEventBean; import com.newrelic.agent.security.intcodeagent.websocket.EventSendPool; +import com.newrelic.agent.security.intcodeagent.websocket.JsonConverter; import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.security.instrumentation.helpers.AppServerInfoHelper; import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; +import com.newrelic.api.agent.security.instrumentation.helpers.SystemCommandUtils; import com.newrelic.api.agent.security.schema.*; import com.newrelic.api.agent.security.schema.helper.DynamoDBRequest; import com.newrelic.api.agent.security.schema.operation.*; @@ -49,12 +53,23 @@ public class Dispatcher implements Callable { public static final String SEPARATOR1 = ", "; public static final String APP_LOCATION = "app-location"; + public static final String REDIS_MODE = "mode"; + public static final String REDIS_ARGUMENTS = "arguments"; + public static final String REDIS_TYPE = "type"; + public static final String SYSCOMMAND_ENVIRONMENT = "environment"; + public static final String SYSCOMMAND_SCRIPT_CONTENT = "script-content"; + public static final String UNABLE_TO_CONVERT_OPERATION_TO_EVENT = "Unable to convert operation to event: %s, %s, %s"; + public static final String COOKIE_VALUE = "value"; + public static final String COOKIE_IS_SECURE = "isSecure"; + public static final String COOKIE_IS_HTTP_ONLY = "isHttpOnly"; + public static final String COOKIE_IS_SAME_SITE_STRICT = "isSameSiteStrict"; private ExitEventBean exitEventBean; private AbstractOperation operation; private SecurityMetaData securityMetaData; private Map extraInfo = new HashMap(); private boolean isNRCode = false; private static AtomicBoolean firstEventSent = new AtomicBoolean(false); + private final String SQL_STORED_PROCEDURE ="SQL_STORED_PROCEDURE"; public ExitEventBean getExitEventBean() { return exitEventBean; @@ -68,6 +83,8 @@ public SecurityMetaData getSecurityMetaData() { return securityMetaData; } + private static Gson GsonUtil = new Gson(); + public Dispatcher(AbstractOperation operation, SecurityMetaData securityMetaData) { this.securityMetaData = securityMetaData; this.operation = operation; @@ -198,6 +215,18 @@ public Object call() throws Exception { XQueryOperation xQueryOperationalBean = (XQueryOperation) operation; eventBean = prepareXQueryInjectionEvent(eventBean, xQueryOperationalBean); break; + case CACHING_DATA_STORE: + if(operation instanceof RedisOperation) { + RedisOperation redisOperation = (RedisOperation) operation; + eventBean = prepareCachingDataStoreEvent(eventBean, redisOperation); + } else if (operation instanceof JCacheOperation) { + JCacheOperation jCacheOperation = (JCacheOperation) operation; + eventBean = prepareJCacheCachingDataStoreEvent(eventBean, jCacheOperation); + } else if (operation instanceof MemcachedOperation) { + MemcachedOperation memcachedOperationalBean = (MemcachedOperation) operation; + eventBean = prepareMemcachedEvent(eventBean, memcachedOperationalBean); + } + break; default: } @@ -221,11 +250,54 @@ public Object call() throws Exception { } // detectDeployedApplication(); } catch (Throwable e) { - e.printStackTrace(); + logger.postLogMessageIfNecessary(LogLevel.WARNING, String.format(UNABLE_TO_CONVERT_OPERATION_TO_EVENT, operation.getApiID(), operation.getSourceMethod(), JsonConverter.getObjectMapper().writeValueAsString(operation.getUserClassEntity())), e, + this.getClass().getName()); } return null; } + private JavaAgentEventBean prepareCachingDataStoreEvent(JavaAgentEventBean eventBean, RedisOperation redisOperation) { + JSONArray params = new JSONArray(); + for (Object data : redisOperation.getArguments()) { + params.add(data); + } + JSONObject command = new JSONObject(); + command.put(REDIS_MODE, redisOperation.getMode()); + command.put(REDIS_ARGUMENTS, params); + command.put(REDIS_TYPE, redisOperation.getType()); + JSONArray parameter = new JSONArray(); + parameter.add(command); + eventBean.setParameters(parameter); + return eventBean; + } + + private JavaAgentEventBean prepareJCacheCachingDataStoreEvent(JavaAgentEventBean eventBean, JCacheOperation jCacheOperation) { + JSONArray params = new JSONArray(); + for (Object data : jCacheOperation.getArguments()) { + if (isPrimitiveType(data.getClass())) { + params.add(data); + } else { + params.add(GsonUtil.toJson(data)); + } + } + + JSONObject command = new JSONObject(); + command.put(REDIS_ARGUMENTS, params); + command.put(REDIS_TYPE, jCacheOperation.getType()); + + JSONArray parameter = new JSONArray(); + parameter.add(command); + eventBean.setParameters(parameter); + eventBean.setEventCategory(jCacheOperation.getCategory()); + return eventBean; + } + + public boolean isPrimitiveType(Class clazz) { + return (clazz.isPrimitive() && clazz != void.class) || clazz == Double.class || clazz == Float.class || clazz == Long.class || + clazz == Integer.class || clazz == Short.class || clazz == Character.class || clazz == Byte.class || clazz == Boolean.class || + clazz == String.class; + } + @Nullable private JavaAgentEventBean processFileOperationEvent(JavaAgentEventBean eventBean, FileOperation fileOperationalBean) { prepareFileEvent(eventBean, fileOperationalBean); @@ -388,6 +460,12 @@ private JavaAgentEventBean prepareSecureCookieEvent(JavaAgentEventBean eventBean SecureCookieOperation secureCookieOperationalBean) { JSONArray params = new JSONArray(); params.add(secureCookieOperationalBean.getValue()); + JSONObject cookie = new JSONObject(); + cookie.put(COOKIE_VALUE, secureCookieOperationalBean.getCookie()); + cookie.put(COOKIE_IS_SECURE, secureCookieOperationalBean.isSecure()); + cookie.put(COOKIE_IS_HTTP_ONLY, secureCookieOperationalBean.isHttpOnly()); + cookie.put(COOKIE_IS_SAME_SITE_STRICT, secureCookieOperationalBean.isSameSiteStrict()); + params.add(cookie); eventBean.setParameters(params); return eventBean; } @@ -438,18 +516,34 @@ private JavaAgentEventBean prepareSQLDbCommandEvent(SQLOperation operation, } params.add(query); eventBean.setParameters(params); - eventBean.setEventCategory(operation.getDbName()); + if (operation.isStoredProcedureCall()) { + eventBean.setEventCategory(SQL_STORED_PROCEDURE); + } else { + eventBean.setEventCategory(operation.getDbName()); + } + return eventBean; } private JavaAgentEventBean prepareSystemCommandEvent(JavaAgentEventBean eventBean, ForkExecOperation operationalBean) { - JSONArray params = new JSONArray(); - params.add(operationalBean.getCommand()); - if (operationalBean.getEnvironment() != null) { - params.add(new JSONObject(operationalBean.getEnvironment())); + try { + List shellScripts = SystemCommandUtils.isShellScriptExecution(operationalBean.getCommand()); + List absolutePaths = SystemCommandUtils.getAbsoluteShellScripts(shellScripts); + SystemCommandUtils.scriptContent(absolutePaths, operationalBean); + JSONArray params = new JSONArray(); + params.add(operationalBean.getCommand()); + JSONObject extras = new JSONObject(); + if (operationalBean.getEnvironment() != null) { + extras.put(SYSCOMMAND_ENVIRONMENT, new JSONObject(operationalBean.getEnvironment())); + } + extras.put(SYSCOMMAND_SCRIPT_CONTENT, operationalBean.getScriptContent()); + params.add(extras); + eventBean.setParameters(params); + return eventBean; + } catch (Throwable e){ + e.printStackTrace(); } - eventBean.setParameters(params); return eventBean; } @@ -479,6 +573,22 @@ private static JavaAgentEventBean prepareNoSQLEvent(JavaAgentEventBean eventBean return eventBean; } + private static JavaAgentEventBean prepareMemcachedEvent(JavaAgentEventBean eventBean, MemcachedOperation memcachedOperationalBean) { + JSONArray params = new JSONArray(); + for (Object data : memcachedOperationalBean.getArguments()) { + params.add(data); + } + JSONObject command = new JSONObject(); + command.put(REDIS_ARGUMENTS, params); + command.put(REDIS_TYPE, memcachedOperationalBean.getType()); + command.put(REDIS_MODE, memcachedOperationalBean.getCommand()); + JSONArray parameter = new JSONArray(); + parameter.add(command); + eventBean.setParameters(parameter); + eventBean.setEventCategory(memcachedOperationalBean.getCategory()); + return eventBean; + } + private static JavaAgentEventBean prepareDynamoDBEvent(JavaAgentEventBean eventBean, DynamoDBOperation dynamoDBOperation) throws ParseException { JSONArray params = new JSONArray(); eventBean.setEventCategory(dynamoDBOperation.getCategory().toString()); @@ -600,6 +710,8 @@ private JavaAgentEventBean setGenericProperties(AbstractOperation objectBean, Ja eventBean.setUserAPIInfo(operation.getUserClassEntity().getUserClassElement().getLineNumber(), operation.getUserClassEntity().getUserClassElement().getClassName(), operation.getUserClassEntity().getUserClassElement().getMethodName()); + eventBean.getLinkingMetadata().put(NR_APM_TRACE_ID, securityMetaData.getCustomAttribute(NR_APM_TRACE_ID, String.class)); + eventBean.getLinkingMetadata().put(NR_APM_SPAN_ID, securityMetaData.getCustomAttribute(NR_APM_SPAN_ID, String.class)); return eventBean; } @@ -608,6 +720,7 @@ private JavaAgentEventBean prepareEvent(HttpRequest httpRequestBean, AgentMetaDa JavaAgentEventBean eventBean = new JavaAgentEventBean(); eventBean.setHttpRequest(httpRequestBean); eventBean.setMetaData(metaData); + eventBean.getMetaData().setAppServerInfo(AppServerInfoHelper.getAppServerInfo()); eventBean.setCaseType(vulnerabilityCaseType.getCaseType()); eventBean.setIsAPIBlocked(metaData.isApiBlocked()); eventBean.setStacktrace(operation.getStackTrace()); diff --git a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/dispatcher/DispatcherPool.java b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/dispatcher/DispatcherPool.java index f20637495..3c970ba66 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/dispatcher/DispatcherPool.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/dispatcher/DispatcherPool.java @@ -9,6 +9,9 @@ import com.newrelic.agent.security.intcodeagent.logging.IAgentConstants; import com.newrelic.agent.security.intcodeagent.models.javaagent.EventStats; import com.newrelic.agent.security.intcodeagent.models.javaagent.ExitEventBean; +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.TraceMetadata; +import com.newrelic.api.agent.security.NewRelicSecurity; import com.newrelic.agent.security.util.AgentUsageMetric; import com.newrelic.agent.security.util.IUtilConstants; import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; @@ -21,6 +24,9 @@ import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; +import static com.newrelic.agent.security.intcodeagent.logging.IAgentConstants.NR_APM_SPAN_ID; +import static com.newrelic.agent.security.intcodeagent.logging.IAgentConstants.NR_APM_TRACE_ID; + public class DispatcherPool { /** @@ -42,6 +48,10 @@ public ThreadPoolExecutor getExecutor() { return executor; } + public int getMaxQueueSize() { + return queueSize; + } + /** * A handler for rejected tasks that throws a @@ -207,6 +217,11 @@ public void dispatchEvent(AbstractOperation operation, SecurityMetaData security .registerEventForProcessedCC(parentId, operation.getExecutionId()); } } + + // Update NR Trace info + TraceMetadata traceMetadata = NewRelic.getAgent().getTraceMetadata(); + securityMetaData.addCustomAttribute(NR_APM_TRACE_ID, traceMetadata.getTraceId()); + securityMetaData.addCustomAttribute(NR_APM_SPAN_ID, traceMetadata.getSpanId()); this.executor.submit(new Dispatcher(operation, new SecurityMetaData(securityMetaData))); } @@ -214,6 +229,12 @@ public void dispatchExitEvent(ExitEventBean exitEventBean) { if (executor.isShutdown()) { return; } + + // Update NR Trace info + SecurityMetaData securityMetaData = NewRelicSecurity.getAgent().getSecurityMetaData(); + TraceMetadata traceMetadata = NewRelic.getAgent().getTraceMetadata(); + securityMetaData.addCustomAttribute(NR_APM_TRACE_ID, traceMetadata.getTraceId()); + securityMetaData.addCustomAttribute(NR_APM_SPAN_ID, traceMetadata.getSpanId()); this.executor.submit(new Dispatcher(exitEventBean)); } @@ -244,5 +265,4 @@ public void shutDownThreadPoolExecutor() { public void reset() { executor.getQueue().clear(); } - } diff --git a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/httpclient/IASTDataTransferRequestProcessor.java b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/httpclient/IASTDataTransferRequestProcessor.java index b96666aec..5b99dc0f2 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/httpclient/IASTDataTransferRequestProcessor.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/httpclient/IASTDataTransferRequestProcessor.java @@ -1,10 +1,9 @@ package com.newrelic.agent.security.instrumentator.httpclient; -import com.newrelic.agent.security.AgentInfo; -import com.newrelic.agent.security.intcodeagent.executor.CustomFutureTask; import com.newrelic.agent.security.intcodeagent.filelogging.FileLoggerThreadPool; import com.newrelic.agent.security.intcodeagent.filelogging.LogLevel; import com.newrelic.agent.security.intcodeagent.models.IASTDataTransferRequest; +import com.newrelic.agent.security.intcodeagent.websocket.JsonConverter; import com.newrelic.agent.security.intcodeagent.websocket.WSClient; import com.newrelic.agent.security.intcodeagent.websocket.WSUtils; import com.newrelic.agent.security.util.AgentUsageMetric; @@ -85,6 +84,7 @@ private void task() { } catch (Throwable e) { logger.log(LogLevel.SEVERE, String.format(UNABLE_TO_SEND_IAST_DATA_REQUEST_DUE_TO_ERROR_S_S, e.toString(), e.getCause().toString()), this.getClass().getName()); logger.log(LogLevel.FINEST, String.format(UNABLE_TO_SEND_IAST_DATA_REQUEST_DUE_TO_ERROR, request), e, this.getClass().getName()); + logger.postLogMessageIfNecessary(LogLevel.SEVERE, String.format(UNABLE_TO_SEND_IAST_DATA_REQUEST_DUE_TO_ERROR, JsonConverter.toJSON(request)), e, this.getClass().getName()); } } @@ -125,9 +125,7 @@ public void startDataRequestSchedule(long delay, TimeUnit timeUnit){ try { stopDataRequestSchedule(true); future = executorService.scheduleWithFixedDelay(this::task, 0, delay, timeUnit); - } catch (Throwable e){ - e.printStackTrace(); - } + } catch (Throwable ignored){} } public void stopDataRequestSchedule(boolean force){ @@ -136,9 +134,7 @@ public void stopDataRequestSchedule(boolean force){ future.cancel(force); future = null; } - } catch (Throwable e) { - e.printStackTrace(); - } + } catch (Throwable ignored) {} } public void setCooldownTillTimestamp(long timestamp) { diff --git a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/httpclient/RestClient.java b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/httpclient/RestClient.java index caa5ba38e..d59fb5ddc 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/httpclient/RestClient.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/httpclient/RestClient.java @@ -114,7 +114,7 @@ public OkHttpClient getClient() { return clientThreadLocal.get(); } - public void fireRequest(Request request, int repeatCount) { + public void fireRequest(Request request, int repeatCount, String fuzzRequestId) { OkHttpClient client = clientThreadLocal.get(); logger.log(LogLevel.FINER, String.format(FIRING_REQUEST_METHOD_S, request.method()), RestClient.class.getName()); @@ -131,10 +131,15 @@ public void fireRequest(Request request, int repeatCount) { } } catch (InterruptedIOException e){ if(repeatCount >= 0){ - fireRequest(request, --repeatCount); + fireRequest(request, --repeatCount, fuzzRequestId); } } catch (IOException e) { logger.log(LogLevel.FINER, String.format(CALL_FAILED_REQUEST_S_REASON, request), e, RestClient.class.getName()); + logger.postLogMessageIfNecessary(LogLevel.WARNING, + String.format(CALL_FAILED_REQUEST_S_REASON, fuzzRequestId), + e, RestRequestProcessor.class.getName()); + + // TODO: Add to fuzz fail count in HC and remove FuzzFailEvent if not needed. FuzzFailEvent fuzzFailEvent = new FuzzFailEvent(AgentInfo.getInstance().getApplicationUUID()); fuzzFailEvent.setFuzzHeader(request.header(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)); EventSendPool.getInstance().sendEvent(fuzzFailEvent); diff --git a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/httpclient/RestRequestProcessor.java b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/httpclient/RestRequestProcessor.java index b185c37a3..de64b9560 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/httpclient/RestRequestProcessor.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/httpclient/RestRequestProcessor.java @@ -4,13 +4,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.newrelic.agent.security.AgentInfo; import com.newrelic.agent.security.instrumentator.os.OsVariablesInstance; +import com.newrelic.agent.security.instrumentator.utils.CallbackUtils; import com.newrelic.agent.security.intcodeagent.filelogging.FileLoggerThreadPool; import com.newrelic.agent.security.intcodeagent.filelogging.LogLevel; import com.newrelic.agent.security.intcodeagent.models.FuzzRequestBean; import com.newrelic.agent.security.intcodeagent.models.javaagent.IntCodeControlCommand; import com.newrelic.agent.security.intcodeagent.websocket.WSUtils; import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper; -import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; import okhttp3.Request; import org.apache.commons.lang3.StringUtils; @@ -21,7 +21,8 @@ */ public class RestRequestProcessor implements Callable { - public static final String NR_CSEC_VALIDATOR_HOME_TMP = "{{NR_CSEC_VALIDATOR_HOME_TMP}}"; + public static final String NR_CSEC_VALIDATOR_HOME_TMP = "/{{NR_CSEC_VALIDATOR_HOME_TMP}}"; + public static final String NR_CSEC_VALIDATOR_HOME_TMP_URL_ENCODED = "%2F%7B%7BNR_CSEC_VALIDATOR_HOME_TMP%7D%7D"; public static final String ERROR_WHILE_PROCESSING_FUZZING_REQUEST_S = "Error while processing fuzzing request : %s"; @@ -63,24 +64,28 @@ public Boolean call() throws InterruptedException { } } String req = StringUtils.replace(controlCommand.getArguments().get(0), NR_CSEC_VALIDATOR_HOME_TMP, OsVariablesInstance.getInstance().getOsVariables().getTmpDirectory()); + req = StringUtils.replace(req, NR_CSEC_VALIDATOR_HOME_TMP_URL_ENCODED, CallbackUtils.urlEncode(OsVariablesInstance.getInstance().getOsVariables().getTmpDirectory())); + httpRequest = objectMapper.readValue(req, FuzzRequestBean.class); httpRequest.getHeaders().put(GenericHelper.CSEC_PARENT_ID, controlCommand.getId()); RestRequestThreadPool.getInstance().removeFromProcessedCC(controlCommand.getId()); Request request = RequestUtils.generateK2Request(httpRequest); if(request != null) { - RestClient.getInstance().fireRequest(request, repeatCount); + RestClient.getInstance().fireRequest(request, repeatCount, controlCommand.getId()); } return true; } catch (JsonProcessingException e){ logger.log(LogLevel.SEVERE, String.format(JSON_PARSING_ERROR_WHILE_PROCESSING_FUZZING_REQUEST_S, controlCommand.getArguments().get(0)), e, RestRequestProcessor.class.getName()); + logger.postLogMessageIfNecessary(LogLevel.SEVERE, + String.format(JSON_PARSING_ERROR_WHILE_PROCESSING_FUZZING_REQUEST_S, controlCommand.getId()), e, RestRequestProcessor.class.getName()); } catch (Throwable e) { logger.log(LogLevel.SEVERE, String.format(ERROR_WHILE_PROCESSING_FUZZING_REQUEST_S, controlCommand.getArguments().get(0)), e, RestRequestProcessor.class.getName()); logger.postLogMessageIfNecessary(LogLevel.SEVERE, - String.format(ERROR_WHILE_PROCESSING_FUZZING_REQUEST_S, controlCommand.getArguments().get(0)), + String.format(ERROR_WHILE_PROCESSING_FUZZING_REQUEST_S, controlCommand.getId()), e, RestRequestProcessor.class.getName()); throw e; } diff --git a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/utils/ApplicationInfoUtils.java b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/utils/ApplicationInfoUtils.java index 75a446213..b96855e82 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/utils/ApplicationInfoUtils.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/utils/ApplicationInfoUtils.java @@ -104,9 +104,7 @@ public static String getPodId() { podId = StringUtils.substringBetween(line, "kubepods-besteffort-pod", ".slice"); } } - } catch (Throwable e) { - e.printStackTrace(); - } + } catch (Throwable ignored) {} return StringUtils.replaceChars(podId, "_", "-"); } diff --git a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/utils/CallbackUtils.java b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/utils/CallbackUtils.java index 33e2c089e..5d59814c7 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/utils/CallbackUtils.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/utils/CallbackUtils.java @@ -11,6 +11,7 @@ import org.unbescape.html.HtmlEscape; import java.net.URLDecoder; +import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.Map.Entry; @@ -104,6 +105,23 @@ public static String urlDecode(String encodedString) { return decodedString; } + /** + * Method to url encode given data. If the + * conversion is not possible, original string is returned. + * + * @param data data string + * @return URL encoded string + */ + public static String urlEncode(String data) { + String decodedString = StringUtils.EMPTY; + try { + decodedString = URLEncoder.encode(data, StandardCharsets.UTF_8.name()); + } catch (Throwable e) { + decodedString = data; + } + return decodedString; + } + static Set getXSSConstructs(String data) { logger.log(LogLevel.FINER, CAME_TO_XSS_CHECK + data, CallbackUtils.class.getName()); diff --git a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/utils/INRSettingsKey.java b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/utils/INRSettingsKey.java index 65e471bd5..aa1df1dd6 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/utils/INRSettingsKey.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/instrumentator/utils/INRSettingsKey.java @@ -20,6 +20,8 @@ public interface INRSettingsKey { String SECURITY_POLICY_ENFORCE = "security.policy.enforce"; String NR_ENTITY_GUID = "entity.guid"; + + String ENTITY_NAME = "entity.name"; String AGENT_RUN_ID = "agent_run_id"; String HOSTNAME = "hostname"; diff --git a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/controlcommand/ControlCommandProcessor.java b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/controlcommand/ControlCommandProcessor.java index b7f24bed4..97ee47008 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/controlcommand/ControlCommandProcessor.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/controlcommand/ControlCommandProcessor.java @@ -51,6 +51,8 @@ public class ControlCommandProcessor implements Runnable { public static final String UPDATED_POLICY_FAILED_VALIDATION_REVERTING_TO_DEFAULT_POLICY_FOR_THE_MODE = "Updated policy failed validation. Reverting to default policy for the mode"; public static final String ERROR_WHILE_PROCESSING_RECONNECTION_CC_S_S = "Error while processing reconnection CC : %s : %s"; public static final String ERROR_WHILE_PROCESSING_RECONNECTION_CC = "Error while processing reconnection CC :"; + public static final String ERROR_WHILE_PROCESSING_RECONNECTION_CC_ID = "Error while processing reconnection CC : %s"; + public static final String UNABLE_TO_PARSE_RECEIVED_DEFAULT_POLICY = "Unable to parse received default policy : "; public static final String ERROR_IN_CONTROL_COMMAND_PROCESSOR = "Error in controlCommandProcessor : "; public static final String ARGUMENTS = "arguments"; @@ -95,6 +97,10 @@ public void run() { } catch (Throwable e) { logger.log(LogLevel.SEVERE, ERROR_IN_CONTROL_COMMAND_PROCESSOR, e, ControlCommandProcessor.class.getSimpleName()); + + logger.postLogMessageIfNecessary(LogLevel.WARNING, + ERROR_IN_CONTROL_COMMAND_PROCESSOR, + e, this.getClass().getName()); return; } @@ -232,6 +238,8 @@ public void run() { } catch (Throwable e) { logger.log(LogLevel.SEVERE, String.format(ERROR_WHILE_PROCESSING_RECONNECTION_CC_S_S, e.getMessage(), e.getCause()), this.getClass().getName()); logger.log(LogLevel.SEVERE, ERROR_WHILE_PROCESSING_RECONNECTION_CC, e, this.getClass().getName()); + logger.postLogMessageIfNecessary(LogLevel.SEVERE, String.format(ERROR_WHILE_PROCESSING_RECONNECTION_CC_ID, controlCommand.getId()), e, + this.getClass().getName()); } break; diff --git a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/controlcommand/ControlCommandProcessorThreadPool.java b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/controlcommand/ControlCommandProcessorThreadPool.java index 155c82ef6..7e4749253 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/controlcommand/ControlCommandProcessorThreadPool.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/controlcommand/ControlCommandProcessorThreadPool.java @@ -9,6 +9,7 @@ public class ControlCommandProcessorThreadPool { + public static final String UNEXPECTED_ERROR_WHILE_PROCESSING_CONTROL_COMMAND = "Unexpected error while processing control command"; /** * Thread pool executor. */ @@ -70,6 +71,8 @@ protected void afterExecute(Runnable r, Throwable t) { future.get(); } } catch (Throwable e) { + logger.postLogMessageIfNecessary(LogLevel.WARNING, UNEXPECTED_ERROR_WHILE_PROCESSING_CONTROL_COMMAND, e, + this.getClass().getName()); } } super.afterExecute(r, t); diff --git a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/filelogging/FileLoggerThreadPool.java b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/filelogging/FileLoggerThreadPool.java index 9f02adc34..36387ceac 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/filelogging/FileLoggerThreadPool.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/filelogging/FileLoggerThreadPool.java @@ -48,8 +48,7 @@ protected void afterExecute(Runnable r, Throwable t) { if (future.isDone()) { future.get(); } - } catch (Throwable e) { - } + } catch (Throwable ignored) {} } super.afterExecute(r, t); } @@ -165,8 +164,8 @@ public void postLogMessageIfNecessary(LogLevel logLevel, String event, Throwable postLogMessage(logLevel, event, exception, caller); } - private LogMessage postLogMessage(LogLevel logLevel, String event, Throwable exception, String caller) { - LogMessage message = new LogMessage(logLevel.name(), event, caller, exception, AgentInfo.getInstance().getLinkingMetadata()); + private LogMessage postLogMessage(LogLevel logLevel, String messageString, Throwable exception, String caller) { + LogMessage message = new LogMessage(logLevel.name(), messageString, caller, exception, AgentInfo.getInstance().getLinkingMetadata()); if (logLevel.getLevel() <= LogLevel.WARNING.getLevel()) { AgentUtils.getInstance().getStatusLogMostRecentErrors().add(JsonConverter.toJSON(message)); } diff --git a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/filelogging/InitLogWriter.java b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/filelogging/InitLogWriter.java index 97fe89a78..a47502bc2 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/filelogging/InitLogWriter.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/filelogging/InitLogWriter.java @@ -92,9 +92,7 @@ private static void createLogFile() { writer.write(String.format(LOG_CONFIGURED_SUCCESSFULLY_MSG, LogLevel.getLevelName(defaultLogLevel), maxFileSize)); writer.flush(); } catch (Throwable e) { - //TODO report to cloud FileLoggerThreadPool.getInstance().setInitLoggingActive(false); - String tmpDir = System.getProperty("java.io.tmpdir"); System.err.println("[NR-CSEC-JA] Unable to create status log file!!! Please find the error in " + tmpDir + File.separator + "NR-CSEC-Logger.err"); try { diff --git a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/filelogging/LogWriter.java b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/filelogging/LogWriter.java index a2624c2ac..d30702658 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/filelogging/LogWriter.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/filelogging/LogWriter.java @@ -76,7 +76,6 @@ private static boolean createLogFile() { } catch (Throwable e) { if (FileLoggerThreadPool.getInstance().isLoggingActive()) { - //TODO report to cloud FileLoggerThreadPool.getInstance().setLoggingActive(false); } String tmpDir = System.getProperty("java.io.tmpdir"); diff --git a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/logging/IAgentConstants.java b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/logging/IAgentConstants.java index b2eb04299..e4334fd58 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/logging/IAgentConstants.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/logging/IAgentConstants.java @@ -568,4 +568,7 @@ public interface IAgentConstants { "Last 5 Heath Checks:\n" + "${last-5-hc}"; String PROCESS_BINARY = "process-binary"; + + String NR_APM_TRACE_ID = "trace.id"; + String NR_APM_SPAN_ID = "span.id"; } \ No newline at end of file diff --git a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/models/collectorconfig/CustomerInfo.java b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/models/collectorconfig/CustomerInfo.java index a049e0a30..ffe25aaf2 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/models/collectorconfig/CustomerInfo.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/models/collectorconfig/CustomerInfo.java @@ -22,6 +22,7 @@ public class CustomerInfo { public CustomerInfo() { } + @JsonIgnore public String getApiAccessorToken() { return apiAccessorToken; } diff --git a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/models/javaagent/AgentBasicInfo.java b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/models/javaagent/AgentBasicInfo.java index 99d27a4af..b09a715df 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/models/javaagent/AgentBasicInfo.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/models/javaagent/AgentBasicInfo.java @@ -7,6 +7,7 @@ import com.newrelic.agent.security.instrumentator.utils.INRSettingsKey; import org.apache.commons.lang3.StringUtils; +import java.util.HashMap; import java.util.Map; import static com.newrelic.agent.security.intcodeagent.logging.IAgentConstants.*; @@ -70,7 +71,7 @@ public AgentBasicInfo() { setBuildNumber(AgentInfo.getInstance().getBuildInfo().getBuildNumber()); setGroupName(AgentConfig.getInstance().getGroupName()); setNodeId(AgentInfo.getInstance().getLinkingMetadata().getOrDefault(INRSettingsKey.NR_ENTITY_GUID, StringUtils.EMPTY)); - setLinkingMetadata(AgentInfo.getInstance().getLinkingMetadata()); + setLinkingMetadata(new HashMap<>(AgentInfo.getInstance().getLinkingMetadata())); if (this instanceof ApplicationInfoBean) { setJsonName(JSON_NAME_APPLICATION_INFO_BEAN); } else if (this instanceof JavaAgentEventBean) { diff --git a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/models/javaagent/JAHealthCheck.java b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/models/javaagent/JAHealthCheck.java index 419ef5a53..b93161a9c 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/models/javaagent/JAHealthCheck.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/models/javaagent/JAHealthCheck.java @@ -84,16 +84,16 @@ public JAHealthCheck(String applicationUUID) { public JAHealthCheck(JAHealthCheck jaHealthCheck) { super(); this.applicationUUID = jaHealthCheck.applicationUUID; - this.invokedHookCount = jaHealthCheck.invokedHookCount; - this.eventDropCount = jaHealthCheck.eventDropCount; - this.eventProcessed = jaHealthCheck.eventProcessed; - this.eventSentCount = jaHealthCheck.eventSentCount; - this.exitEventSentCount = jaHealthCheck.exitEventSentCount; - this.httpRequestCount = jaHealthCheck.httpRequestCount; - this.eventRejectionCount = jaHealthCheck.eventRejectionCount; - this.eventProcessingErrorCount = jaHealthCheck.eventProcessingErrorCount; - this.eventSendRejectionCount = jaHealthCheck.eventSendRejectionCount; - this.eventSendErrorCount = jaHealthCheck.eventSendErrorCount; + this.invokedHookCount = new AtomicInteger(jaHealthCheck.invokedHookCount.intValue()); + this.eventDropCount = new AtomicInteger(jaHealthCheck.eventDropCount.intValue()); + this.eventProcessed = new AtomicInteger(jaHealthCheck.eventProcessed.intValue()); + this.eventSentCount = new AtomicInteger(jaHealthCheck.eventSentCount.intValue()); + this.exitEventSentCount = new AtomicInteger(jaHealthCheck.exitEventSentCount.intValue()); + this.httpRequestCount = new AtomicInteger(jaHealthCheck.httpRequestCount.intValue()); + this.eventRejectionCount = new AtomicInteger(jaHealthCheck.eventRejectionCount.intValue()); + this.eventProcessingErrorCount = new AtomicInteger(jaHealthCheck.eventProcessingErrorCount.intValue()); + this.eventSendRejectionCount = new AtomicInteger(jaHealthCheck.eventSendRejectionCount.intValue()); + this.eventSendErrorCount = new AtomicInteger(jaHealthCheck.eventSendErrorCount.intValue()); this.raspEventStats = new EventStats(jaHealthCheck.raspEventStats); this.iastEventStats = new EventStats(jaHealthCheck.iastEventStats); this.exitEventStats = new EventStats(jaHealthCheck.exitEventStats); diff --git a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/models/javaagent/LogMessage.java b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/models/javaagent/LogMessage.java index 8c6c383f0..4e09c7621 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/models/javaagent/LogMessage.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/models/javaagent/LogMessage.java @@ -10,7 +10,7 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class LogMessage { - private String jsonName = "log-message"; + private String jsonName = "critical-messages"; private String applicationUUID = AgentInfo.getInstance().getApplicationUUID(); @@ -25,6 +25,8 @@ public class LogMessage { private Map linkingMetadata; + private String threadName; + public LogMessage(String level, String message, String caller, Throwable exception, Map linkingMetadata) { this.timestamp = Instant.now().toEpochMilli(); this.level = level; @@ -34,6 +36,7 @@ public LogMessage(String level, String message, String caller, Throwable excepti if (exception != null) { this.exception = new LogMessageException(exception, 0, 1); } + this.threadName = Thread.currentThread().getName(); } public Long getTimestamp() { @@ -77,6 +80,15 @@ public void setApplicationUUID(String applicationUUID) { this.applicationUUID = applicationUUID; } + + public String getThreadName() { + return threadName; + } + + public void setThreadName(String threadName) { + this.threadName = threadName; + } + @Override public String toString() { return JsonConverter.toJSON(this); diff --git a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/models/javaagent/LogMessageException.java b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/models/javaagent/LogMessageException.java index 0835965c0..5a7455e8a 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/models/javaagent/LogMessageException.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/models/javaagent/LogMessageException.java @@ -7,6 +7,8 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class LogMessageException { + private String type; + private String message; private LogMessageException cause; @@ -14,6 +16,7 @@ public class LogMessageException { private String[] stackTrace; public LogMessageException(Throwable exception, int nestingLevel, int maxNestingLevel) { + this.type = exception.getClass().getName(); this.message = exception.getMessage(); StackTraceElement[] trace = exception.getStackTrace(); this.stackTrace = new String[trace.length]; @@ -37,6 +40,10 @@ public String[] getStackTrace() { return stackTrace; } + public String getType() { + return type; + } + public String toString() { return JsonConverter.toJSON(this); } diff --git a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/websocket/EventSendPool.java b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/websocket/EventSendPool.java index e98092e8f..0922b3355 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/websocket/EventSendPool.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/websocket/EventSendPool.java @@ -1,7 +1,6 @@ package com.newrelic.agent.security.intcodeagent.websocket; import com.newrelic.agent.security.AgentInfo; -import com.newrelic.agent.security.instrumentator.dispatcher.DispatcherPool; import com.newrelic.agent.security.instrumentator.httpclient.RestRequestThreadPool; import com.newrelic.agent.security.intcodeagent.executor.CustomFutureTask; import com.newrelic.agent.security.intcodeagent.executor.CustomThreadPoolExecutor; @@ -13,12 +12,12 @@ import com.newrelic.agent.security.util.AgentUsageMetric; import com.newrelic.agent.security.util.IUtilConstants; -import java.util.Map; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; public class EventSendPool { + public static final int QUEUE_SIZE = 1500; /** * Thread pool executor. */ @@ -30,7 +29,7 @@ public class EventSendPool { private EventSendPool() { // load the settings - int queueSize = 1500; + int queueSize = QUEUE_SIZE; int maxPoolSize = 1; int corePoolSize = 1; long keepAliveTime = 60; @@ -67,6 +66,10 @@ public Thread newThread(Runnable r) { }); } + public int getMaxQueueSize() { + return QUEUE_SIZE; + } + private static final class InstanceHolder { static final EventSendPool instance = new EventSendPool(); } @@ -197,7 +200,7 @@ private void incrementCount(Runnable r, String type) { eventStats.incrementRejectedCount(); break; default: - logger.log(LogLevel.FINEST, String.format("Couldn't update event matric for task :%s and type : %s", r, type), DispatcherPool.class.getName()); + logger.log(LogLevel.FINEST, String.format("Couldn't update event matric for task :%s and type : %s", r, type), EventSendPool.class.getName()); } } diff --git a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/websocket/WSClient.java b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/websocket/WSClient.java index cf630e6d7..dfe160f31 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/websocket/WSClient.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/websocket/WSClient.java @@ -131,6 +131,8 @@ private WSClient() throws URISyntaxException { this.setConnectionLostTimeout(30); this.addHeader("NR-CSEC-CONNECTION-TYPE", "LANGUAGE_COLLECTOR"); this.addHeader("NR-AGENT-RUN-TOKEN", AgentInfo.getInstance().getLinkingMetadata().getOrDefault(INRSettingsKey.AGENT_RUN_ID_LINKING_METADATA, StringUtils.EMPTY)); + this.addHeader("NR-CSEC-ENTITY-GUID", AgentInfo.getInstance().getLinkingMetadata().getOrDefault(INRSettingsKey.NR_ENTITY_GUID, StringUtils.EMPTY)); + this.addHeader("NR-CSEC-ENTITY-NAME", AgentInfo.getInstance().getLinkingMetadata().getOrDefault(INRSettingsKey.ENTITY_NAME, StringUtils.EMPTY)); this.addHeader("NR-LICENSE-KEY", AgentConfig.getInstance().getConfig().getCustomerInfo().getApiAccessorToken()); this.addHeader("NR-CSEC-VERSION", AgentInfo.getInstance().getBuildInfo().getCollectorVersion()); this.addHeader("NR-CSEC-COLLECTOR-TYPE", "JAVA"); diff --git a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/util/AgentUsageMetric.java b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/util/AgentUsageMetric.java index f08d988c4..ac536f94d 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/util/AgentUsageMetric.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/util/AgentUsageMetric.java @@ -7,11 +7,11 @@ public class AgentUsageMetric { public static Boolean isRASPProcessingActive() { if(EventSendPool.getInstance().getExecutor().getQueue().size() > - EventSendPool.getInstance().getExecutor().getMaximumPoolSize()/2){ + EventSendPool.getInstance().getMaxQueueSize()/2){ return false; } if(DispatcherPool.getInstance().getExecutor().getQueue().size() > - DispatcherPool.getInstance().getExecutor().getMaximumPoolSize()*2/3){ + DispatcherPool.getInstance().getMaxQueueSize()*2/3){ return false; } return true; @@ -19,11 +19,11 @@ public static Boolean isRASPProcessingActive() { public static Boolean isIASTRequestProcessingActive() { if(EventSendPool.getInstance().getExecutor().getQueue().size() > - EventSendPool.getInstance().getExecutor().getMaximumPoolSize()*2/3){ + EventSendPool.getInstance().getMaxQueueSize()*2/3){ return false; } if(DispatcherPool.getInstance().getExecutor().getQueue().size() > - DispatcherPool.getInstance().getExecutor().getMaximumPoolSize()*0.75){ + DispatcherPool.getInstance().getMaxQueueSize()*0.75){ return false; } return true; diff --git a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/util/IUtilConstants.java b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/util/IUtilConstants.java index 0536ed9e4..5035aaabe 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/util/IUtilConstants.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/util/IUtilConstants.java @@ -27,4 +27,8 @@ public interface IUtilConstants { String SENT = "SENT"; String REJECTED = "REJECTED"; String NR_LOG_DAILY_ROLLOVER_PERIOD = "log.rollover.period"; + String APPLICATION_DIRECTORY = "APPLICATION_DIRECTORY"; + + String SERVER_BASE_DIRECTORY = "SERVER_BASE_DIRECTORY"; + String SAME_SITE_COOKIES = "SAME_SITE_COOKIES"; } diff --git a/newrelic-security-agent/src/main/java/com/newrelic/api/agent/security/Agent.java b/newrelic-security-agent/src/main/java/com/newrelic/api/agent/security/Agent.java index 9c7cb15ec..32890a242 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/api/agent/security/Agent.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/api/agent/security/Agent.java @@ -3,45 +3,32 @@ import com.fasterxml.jackson.dataformat.javaprop.JavaPropsMapper; import com.newrelic.agent.security.AgentConfig; import com.newrelic.agent.security.AgentInfo; -import com.newrelic.agent.security.instrumentator.dispatcher.Dispatcher; import com.newrelic.agent.security.instrumentator.dispatcher.DispatcherPool; -import com.newrelic.agent.security.instrumentator.httpclient.RestRequestThreadPool; import com.newrelic.agent.security.instrumentator.os.OsVariablesInstance; -import com.newrelic.agent.security.instrumentator.utils.AgentUtils; -import com.newrelic.agent.security.instrumentator.utils.ApplicationInfoUtils; -import com.newrelic.agent.security.instrumentator.utils.CollectorConfigurationUtils; -import com.newrelic.agent.security.instrumentator.utils.ExecutionIDGenerator; -import com.newrelic.agent.security.instrumentator.utils.HashGenerator; -import com.newrelic.agent.security.instrumentator.utils.INRSettingsKey; +import com.newrelic.agent.security.instrumentator.utils.*; import com.newrelic.agent.security.intcodeagent.constants.AgentServices; import com.newrelic.agent.security.intcodeagent.filelogging.FileLoggerThreadPool; import com.newrelic.agent.security.intcodeagent.filelogging.LogFileHelper; import com.newrelic.agent.security.intcodeagent.filelogging.LogLevel; import com.newrelic.agent.security.intcodeagent.logging.HealthCheckScheduleThread; import com.newrelic.agent.security.intcodeagent.logging.IAgentConstants; -import com.newrelic.agent.security.intcodeagent.models.javaagent.ApplicationURLMappings; import com.newrelic.agent.security.intcodeagent.models.javaagent.ExitEventBean; import com.newrelic.agent.security.intcodeagent.properties.BuildInfo; import com.newrelic.agent.security.intcodeagent.schedulers.FileCleaner; import com.newrelic.agent.security.intcodeagent.schedulers.SchedulerHelper; import com.newrelic.agent.security.intcodeagent.utils.CommonUtils; import com.newrelic.agent.security.intcodeagent.websocket.*; +import com.newrelic.agent.security.util.IUtilConstants; import com.newrelic.api.agent.NewRelic; import com.newrelic.api.agent.Transaction; +import com.newrelic.api.agent.security.instrumentation.helpers.AppServerInfoHelper; import com.newrelic.api.agent.security.instrumentation.helpers.LowSeverityHelper; -import com.newrelic.api.agent.security.instrumentation.helpers.URLMappingsHelper; -import com.newrelic.api.agent.security.schema.AbstractOperation; -import com.newrelic.api.agent.security.schema.AgentMetaData; -import com.newrelic.api.agent.security.schema.ApplicationURLMapping; -import com.newrelic.api.agent.security.schema.HttpRequest; -import com.newrelic.api.agent.security.schema.K2RequestIdentifier; -import com.newrelic.api.agent.security.schema.SecurityMetaData; -import com.newrelic.api.agent.security.schema.UserClassEntity; -import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; +import com.newrelic.api.agent.security.schema.*; import com.newrelic.api.agent.security.schema.operation.RXSSOperation; import com.newrelic.api.agent.security.schema.policy.AgentPolicy; import org.apache.commons.lang3.StringUtils; +import java.io.File; import java.io.IOException; import java.lang.instrument.Instrumentation; import java.time.Instant; @@ -53,28 +40,16 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; -import static com.newrelic.agent.security.intcodeagent.logging.IAgentConstants.AGENT_INIT_LOG_STEP_FIVE_END; -import static com.newrelic.agent.security.intcodeagent.logging.IAgentConstants.COM_SUN; -import static com.newrelic.agent.security.intcodeagent.logging.IAgentConstants.STARTED_MODULE_LOG; -import static com.newrelic.agent.security.intcodeagent.logging.IAgentConstants.SUN_REFLECT; +import static com.newrelic.agent.security.intcodeagent.logging.IAgentConstants.*; public class Agent implements SecurityAgent { - private static final String AGENT_INIT_SUCCESSFUL = "[STEP-2][PROTECTION][COMPLETE] Protecting new process with PID %s and UUID %s : %s."; private static final String EVENT_ZERO_PROCESSED = "[EVENT] First event processed : %s"; - public static final String SCHEDULING_FOR_EVENT_RESPONSE_OF = "Scheduling for event response of : "; - public static final String EVENT_RESPONSE_TIMEOUT_FOR = "Event response timeout for : "; - public static final String ERROR_WHILE_BLOCKING_FOR_RESPONSE = "Error while blocking for response: "; - public static final String ERROR = "Error: "; - public static final String CRITICAL_ERROR_UNABLE_TO_READ_BUILD_INFO_AND_VERSION_S_S = "CSEC Critical error. Unable to read buildInfo and version: {1} : {2}"; + public static final String CRITICAL_ERROR_UNABLE_TO_READ_BUILD_INFO_AND_VERSION_S_S = "CSEC Critical error. Unable to read buildInfo and version: %s : %s"; public static final String CRITICAL_ERROR_UNABLE_TO_READ_BUILD_INFO_AND_VERSION = "CSEC Critical error. Unable to read buildInfo and version: "; public static final String DROPPING_EVENT_AS_IT_WAS_GENERATED_BY_K_2_INTERNAL_API_CALL = "Dropping event as it was generated by agent internal API call : "; - private static AtomicBoolean firstEventProcessed = new AtomicBoolean(false); - - private static final Object lock = new Object(); - - private static Agent instance; + private static final AtomicBoolean firstEventProcessed = new AtomicBoolean(false); private AgentInfo info; @@ -87,15 +62,12 @@ public class Agent implements SecurityAgent { private java.net.URL agentJarURL; private Instrumentation instrumentation; + private static final class InstanceHolder { + static final Agent instance = new Agent(); + } + public static SecurityAgent getInstance() { - if(instance == null) { - synchronized (lock){ - if(instance == null){ - instance = new Agent(); - } - } - } - return instance; + return InstanceHolder.instance; } private Agent(){ @@ -170,6 +142,9 @@ private BuildInfo readCollectorBuildInfo() { readValue(CommonUtils.getResourceStreamFromAgentJar("Agent.properties"), BuildInfo.class); } catch (Throwable e) { logger.log(LogLevel.SEVERE, String.format(CRITICAL_ERROR_UNABLE_TO_READ_BUILD_INFO_AND_VERSION_S_S, e.getMessage(), e.getCause()), this.getClass().getName()); + logger.postLogMessageIfNecessary(LogLevel.SEVERE, + String.format(CRITICAL_ERROR_UNABLE_TO_READ_BUILD_INFO_AND_VERSION_S_S, e.getMessage(), e.getCause()), + e, this.getClass().getName()); logger.log(LogLevel.FINER, CRITICAL_ERROR_UNABLE_TO_READ_BUILD_INFO_AND_VERSION, e, this.getClass().getName()); } return buildInfo; @@ -272,10 +247,11 @@ public void registerOperation(AbstractOperation operation) { if (operation instanceof RXSSOperation) { operation.setStackTrace(securityMetaData.getMetaData().getServiceTrace()); } else { - operation.setStackTrace(Thread.currentThread().getStackTrace()); + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + operation.setStackTrace(Arrays.copyOfRange(trace, 1, trace.length)); } - if(checkIfNRGeneratedEvent(operation, securityMetaData)) { + if(checkIfNRGeneratedEvent(operation)) { logger.log(LogLevel.FINEST, DROPPING_EVENT_AS_IT_WAS_GENERATED_BY_K_2_INTERNAL_API_CALL + JsonConverter.toJSON(operation), Agent.class.getName()); @@ -285,8 +261,8 @@ public void registerOperation(AbstractOperation operation) { logIfIastScanForFirstTime(securityMetaData.getFuzzRequestIdentifier(), securityMetaData.getRequest()); setRequiredStackTrace(operation, securityMetaData); - setUserClassEntity(operation, securityMetaData); processStackTrace(operation); + operation.setUserClassEntity(setUserClassEntity(operation, securityMetaData)); // boolean blockNeeded = checkIfBlockingNeeded(operation.getApiID()); // securityMetaData.getMetaData().setApiBlocked(blockNeeded); if (needToGenerateEvent(operation.getApiID())) { @@ -313,7 +289,7 @@ private void logIfIastScanForFirstTime(K2RequestIdentifier fuzzRequestIdentifier } } - private static boolean checkIfNRGeneratedEvent(AbstractOperation operation, SecurityMetaData securityMetaData) { + private static boolean checkIfNRGeneratedEvent(AbstractOperation operation) { for (int i = 1, j = 0; i < operation.getStackTrace().length; i++) { // Only remove consecutive top com.newrelic and com.nr. elements from stack. if (i - 1 == j && StringUtils.startsWithAny(operation.getStackTrace()[i].getClassName(), "com.newrelic.", "com.nr.")) { @@ -332,11 +308,30 @@ && getInstance().getCurrentPolicy().getProtectionMode().getApiBlocking().getEnab ); } - private void setUserClassEntity(AbstractOperation operation, SecurityMetaData securityMetaData) { + private UserClassEntity setUserClassEntity(AbstractOperation operation, SecurityMetaData securityMetaData) { UserClassEntity userClassEntity = new UserClassEntity(); - userClassEntity.setUserClassElement(operation.getStackTrace()[operation.getStackTrace().length - 2]); - userClassEntity.setCalledByUserCode(securityMetaData.getMetaData().isUserLevelServiceMethodEncountered()); - operation.setUserClassEntity(userClassEntity); + StackTraceElement userStackTraceElement = null; + if(securityMetaData.getMetaData().getServiceTrace() != null && securityMetaData.getMetaData().getServiceTrace().length > 0){ + userStackTraceElement = securityMetaData.getMetaData().getServiceTrace()[0]; + } + + for (int i = 0; i < operation.getStackTrace().length; i++) { + StackTraceElement stackTraceElement = operation.getStackTrace()[i]; + if(userStackTraceElement != null){ + if(StringUtils.equals(stackTraceElement.getClassName(), userStackTraceElement.getClassName()) + && StringUtils.equals(stackTraceElement.getMethodName(), userStackTraceElement.getMethodName())){ + userClassEntity.setUserClassElement(stackTraceElement); + userClassEntity.setCalledByUserCode(securityMetaData.getMetaData().isUserLevelServiceMethodEncountered()); + return userClassEntity; + } + } + // TODO: the `if` should be `else if` please check crypto case BenchmarkTest01978. service trace is being registered from doSomething() + if( i+1 < operation.getStackTrace().length && StringUtils.equals(operation.getSourceMethod(), stackTraceElement.toString())){ + userClassEntity.setUserClassElement(operation.getStackTrace()[i + 1]); + userClassEntity.setCalledByUserCode(securityMetaData.getMetaData().isUserLevelServiceMethodEncountered()); + } + } + return userClassEntity; } private void setRequiredStackTrace(AbstractOperation operation, SecurityMetaData securityMetaData) { @@ -352,9 +347,8 @@ private static void processStackTrace(AbstractOperation operation) { ArrayList newTraceForIdCalc = new ArrayList<>(stackTrace.length); - resetFactor++; - boolean markedForRemoval = false; - for (int i = 1, j = 0; i < stackTrace.length; i++) { + boolean markedForRemoval; + for (int i = 0, j = -1; i < stackTrace.length; i++) { markedForRemoval = false; // Only remove consecutive top com.newrelic and com.nr. elements from stack. @@ -399,7 +393,6 @@ private static void processStackTrace(AbstractOperation operation) { private static void setAPIId(AbstractOperation operation, List traceForIdCalc, VulnerabilityCaseType vulnerabilityCaseType) { try { traceForIdCalc.add(operation.getSourceMethod().hashCode()); - traceForIdCalc.add(operation.getUserClassEntity().getUserClassElement().hashCode()); operation.setApiID(vulnerabilityCaseType.getCaseType() + "-" + HashGenerator.getXxHash64Digest(traceForIdCalc.stream().mapToInt(Integer::intValue).toArray())); } catch (IOException e) { operation.setApiID("UNDEFINED"); @@ -453,9 +446,7 @@ public SecurityMetaData getSecurityMetaData() { return (SecurityMetaData) meta; } } - } catch (Throwable e) { -// e.printStackTrace(); - } + } catch (Throwable ignored) {} return new SecurityMetaData(); } @@ -484,7 +475,7 @@ public AgentConfig getConfig() { } public static java.net.URL getAgentJarURL() { - return instance.agentJarURL; + return InstanceHolder.instance.agentJarURL; } public boolean isInitialised() { @@ -502,6 +493,47 @@ public Instrumentation getInstrumentation() { @Override public boolean isLowPriorityInstrumentationEnabled() { - return NewRelic.getAgent().getConfig().getValue(LowSeverityHelper.LOW_SEVERITY_HOOKS_ENABLED, LowSeverityHelper.DEFAULT); + return NewRelicSecurity.isHookProcessingActive() && LowSeverityHelper.getIsLowSeverityhHooksEnabled() && NewRelic.getAgent().getConfig().getValue(LowSeverityHelper.LOW_SEVERITY_HOOKS_ENABLED, LowSeverityHelper.DEFAULT); + } + + @Override + public void setServerInfo(String key, String value) { + AppServerInfo appServerInfo = AppServerInfoHelper.getAppServerInfo(); + switch (key) { + case IUtilConstants.APPLICATION_DIRECTORY: + File appBase = new File(value); + if(appBase.isAbsolute()){ + appServerInfo.setApplicationDirectory(value); + } else if(StringUtils.isNotBlank(appServerInfo.getServerBaseDirectory())) { + appServerInfo.setApplicationDirectory(new File(appServerInfo.getServerBaseDirectory(), value).getAbsolutePath()); + } else if(appBase.isDirectory()) { + appServerInfo.setApplicationDirectory(appBase.getAbsolutePath()); + } + break; + case IUtilConstants.SERVER_BASE_DIRECTORY: + appServerInfo.setServerBaseDirectory(value); + break; + case IUtilConstants.SAME_SITE_COOKIES: + appServerInfo.setSameSiteCookies(value); + break; + default: + break; + } + + } + + @Override + public String getServerInfo(String key) { + AppServerInfo appServerInfo = AppServerInfoHelper.getAppServerInfo(); + switch (key) { + case IUtilConstants.APPLICATION_DIRECTORY: + return appServerInfo.getApplicationDirectory(); + case IUtilConstants.SERVER_BASE_DIRECTORY: + return appServerInfo.getServerBaseDirectory(); + case IUtilConstants.SAME_SITE_COOKIES: + return appServerInfo.getSameSiteCookies(); + default: + return null; + } } } \ No newline at end of file diff --git a/newrelic-security-api-test-impl/src/main/java/com/newrelic/api/agent/security/Agent.java b/newrelic-security-api-test-impl/src/main/java/com/newrelic/api/agent/security/Agent.java index 2b57c8bcb..ac0541400 100644 --- a/newrelic-security-api-test-impl/src/main/java/com/newrelic/api/agent/security/Agent.java +++ b/newrelic-security-api-test-impl/src/main/java/com/newrelic/api/agent/security/Agent.java @@ -11,6 +11,7 @@ import java.net.URL; import java.time.Instant; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -64,7 +65,8 @@ public void registerOperation(AbstractOperation operation) { operation.setExecutionId(executionId); operation.setApiID(apiId); operation.setStartTime(Instant.now().toEpochMilli()); - operation.setStackTrace(Thread.currentThread().getStackTrace()); + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + operation.setStackTrace(Arrays.copyOfRange(trace, 1, trace.length)); this.getSecurityMetaData().getCustomAttribute(OPERATIONS, List.class).add(operation); } @@ -137,4 +139,14 @@ public Instrumentation getInstrumentation() { public boolean isLowPriorityInstrumentationEnabled() { return true; } + + @Override + public void setServerInfo(String key, String value) { + //TODO Ishika please fill this as per your needs + } + + @Override + public String getServerInfo(String key) { + return null; + } } \ No newline at end of file diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/NoOpAgent.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/NoOpAgent.java index 9fd955bf6..07e7f46ec 100644 --- a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/NoOpAgent.java +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/NoOpAgent.java @@ -78,4 +78,12 @@ public boolean isLowPriorityInstrumentationEnabled() { return false; } + @Override + public void setServerInfo(String key, String value) {} + + @Override + public String getServerInfo(String key) { + return null; + } + } diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/SecurityAgent.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/SecurityAgent.java index dbb89481e..20ad7d483 100644 --- a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/SecurityAgent.java +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/SecurityAgent.java @@ -45,4 +45,8 @@ public interface SecurityAgent { Instrumentation getInstrumentation(); boolean isLowPriorityInstrumentationEnabled(); + + void setServerInfo(String key, String value); + + String getServerInfo(String key); } diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/AppServerInfoHelper.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/AppServerInfoHelper.java new file mode 100644 index 000000000..ef6e6410f --- /dev/null +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/AppServerInfoHelper.java @@ -0,0 +1,12 @@ +package com.newrelic.api.agent.security.instrumentation.helpers; + +import com.newrelic.api.agent.security.schema.AppServerInfo; + +public class AppServerInfoHelper { + + private static AppServerInfo appServerInfo = new AppServerInfo(); + + public static AppServerInfo getAppServerInfo() { + return appServerInfo; + } +} diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/FileHelper.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/FileHelper.java index 24442e0ec..cc7bc614b 100644 --- a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/FileHelper.java +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/FileHelper.java @@ -1,12 +1,18 @@ package com.newrelic.api.agent.security.instrumentation.helpers; import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.schema.StringUtils; import com.newrelic.api.agent.security.schema.operation.FileIntegrityOperation; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Paths; +import java.nio.file.attribute.PosixFileAttributes; +import java.nio.file.attribute.PosixFilePermission; import java.util.Arrays; import java.util.List; +import java.util.Set; public class FileHelper { @@ -85,8 +91,19 @@ public static FileIntegrityOperation createEntryOfFileIntegrity(String fileName, String extension = getFileExtension(file); if (SOURCE_EXENSIONS.contains(extension) && !NewRelicSecurity.getAgent().getSecurityMetaData().getFileLocalMap().containsKey(fileName)) { + long lastModified = file.exists()? file.lastModified() : -1; + String permissions = StringUtils.EMPTY; + try { + if(file.exists()) { + PosixFileAttributes fileAttributes = Files.readAttributes(Paths.get(file.getPath()), PosixFileAttributes.class); + Set permissionSet = fileAttributes.permissions(); + permissions = permissionSet.toString(); + } + } catch (IOException e) { + } + long fileLength = file.length(); FileIntegrityOperation fbean = new FileIntegrityOperation(file.exists(), fileName, className, - methodName); + methodName, lastModified, permissions, fileLength); NewRelicSecurity.getAgent().getSecurityMetaData().getFileLocalMap().put(fileName, fbean); return fbean; @@ -99,7 +116,7 @@ public static void checkEntryOfFileIntegrity(List fileNames) { File file = Paths.get(fileName).toFile(); if(NewRelicSecurity.getAgent().getSecurityMetaData().getFileLocalMap().containsKey(fileName)){ FileIntegrityOperation fbean = NewRelicSecurity.getAgent().getSecurityMetaData().getFileLocalMap().get(fileName); - if(!fbean.getExists().equals(file.exists())){ + if(fbean.isIntegrityBreached(file)){ NewRelicSecurity.getAgent().registerOperation(fbean); } } diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/GenericHelper.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/GenericHelper.java index c900be431..6b6566f37 100644 --- a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/GenericHelper.java +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/GenericHelper.java @@ -2,8 +2,15 @@ import com.newrelic.api.agent.security.NewRelicSecurity; +import java.util.regex.Pattern; + public class GenericHelper { + public static Pattern QUOTED_STRING_PATTERN = Pattern.compile("((\\\\)*?('|\\\"))(([\\s\\S]*?)(?:(?=(\\\\?))\\6.)*?)\\1", + Pattern.MULTILINE | Pattern.CASE_INSENSITIVE | Pattern.DOTALL); + public static Pattern STORED_PROCEDURE_PATTERN = Pattern.compile("(call\\s+[a-zA-Z0-9_\\$]+\\(.*?\\))", + Pattern.MULTILINE | Pattern.CASE_INSENSITIVE | Pattern.DOTALL); public static final String CSEC_PARENT_ID = "nr-csec-parent-id"; + public static final String NR_SEC_CUSTOM_SPRING_REDIS_ATTR = "SPRING-DATA-REDIS"; public static boolean skipExistsEvent() { if (!(NewRelicSecurity.getAgent().getCurrentPolicy().getVulnerabilityScan().getEnabled() && diff --git a/instrumentation-security/java-io-inputstream-jdk8/src/main/java/com/newrelic/agent/security/instrumentation/javaio/InputStreamHelper.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/InputStreamHelper.java similarity index 93% rename from instrumentation-security/java-io-inputstream-jdk8/src/main/java/com/newrelic/agent/security/instrumentation/javaio/InputStreamHelper.java rename to newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/InputStreamHelper.java index 396466e06..307856321 100644 --- a/instrumentation-security/java-io-inputstream-jdk8/src/main/java/com/newrelic/agent/security/instrumentation/javaio/InputStreamHelper.java +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/InputStreamHelper.java @@ -1,4 +1,4 @@ -package com.newrelic.agent.security.instrumentation.javaio; +package com.newrelic.api.agent.security.instrumentation.helpers; import com.newrelic.api.agent.security.NewRelicSecurity; diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/LowSeverityHelper.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/LowSeverityHelper.java index df770a59b..9637caa45 100644 --- a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/LowSeverityHelper.java +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/LowSeverityHelper.java @@ -5,22 +5,24 @@ import com.newrelic.api.agent.security.schema.SecurityMetaData; import com.newrelic.api.agent.security.schema.StringUtils; -import java.security.MessageDigest; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; public class LowSeverityHelper { + public static final String LOW_SEVERITY_HOOKS_ENABLED = "security.low-priority-instrumentation.enabled"; - public static final boolean DEFAULT = false; + public static final boolean DEFAULT = true; + private static AtomicBoolean isLowSeverityhHooksEnabled = new AtomicBoolean(false); private static Set encounteredLowSeverityEventURIHash = ConcurrentHashMap.newKeySet(); - public static boolean addLowSeverityEventToEncounteredList(Integer owaspEventApiId) { - return encounteredLowSeverityEventURIHash.add(owaspEventApiId); + public static boolean addLowSeverityEventToEncounteredList(Integer urlHashCode, String method) { + return encounteredLowSeverityEventURIHash.add(StringUtils.join(urlHashCode, method).hashCode()); } - public static boolean checkIfLowSeverityEventAlreadyEncountered(Integer eventApiId) { - return encounteredLowSeverityEventURIHash.contains(eventApiId); + public static boolean checkIfLowSeverityEventAlreadyEncountered(Integer urlHashCode, String method) { + return encounteredLowSeverityEventURIHash.contains(StringUtils.join(urlHashCode, method).hashCode()); } public static void clearLowSeverityEventFilter() { @@ -30,18 +32,28 @@ public static void clearLowSeverityEventFilter() { public static boolean addRrequestUriToEventFilter(HttpRequest request) { if(request!= null && StringUtils.isNotBlank(request.getUrl())) { - return addLowSeverityEventToEncounteredList(request.getUrl().hashCode()); + return addLowSeverityEventToEncounteredList(request.getUrl().hashCode(), request.getMethod()); } return false; } public static boolean isOwaspHookProcessingNeeded(){ SecurityMetaData securityMetaData = NewRelicSecurity.getAgent().getSecurityMetaData(); - if(securityMetaData != null) { + if(NewRelicSecurity.isHookProcessingActive() && securityMetaData != null && !securityMetaData.getRequest().isEmpty()) { String requestURL = securityMetaData.getRequest().getUrl(); return (securityMetaData.getFuzzRequestIdentifier() != null && securityMetaData.getFuzzRequestIdentifier().getK2Request()) - || (StringUtils.isNotBlank(requestURL) && !LowSeverityHelper.checkIfLowSeverityEventAlreadyEncountered(requestURL.hashCode())); + || (StringUtils.isNotBlank(requestURL) && !LowSeverityHelper.checkIfLowSeverityEventAlreadyEncountered(requestURL.hashCode(), securityMetaData.getRequest().getMethod())); } return false; } + + public static void enableLowSeverityHooks(String group) { + if(StringUtils.equals(group, "IAST")) { + isLowSeverityhHooksEnabled.set(true); + } + } + + public static boolean getIsLowSeverityhHooksEnabled() { + return isLowSeverityhHooksEnabled.get(); + } } diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/ServletHelper.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/ServletHelper.java index 8d8081ff2..df3205e75 100644 --- a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/ServletHelper.java +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/ServletHelper.java @@ -8,6 +8,8 @@ import java.io.File; import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Arrays; @@ -17,7 +19,8 @@ public class ServletHelper { public static final String SEPARATOR_SEMICOLON = ":IAST:"; - public static final String NR_CSEC_VALIDATOR_HOME_TMP = "{{NR_CSEC_VALIDATOR_HOME_TMP}}"; + public static final String NR_CSEC_VALIDATOR_HOME_TMP_URL_ENCODED = "%7B%7BNR_CSEC_VALIDATOR_HOME_TMP%7D%7D"; + public static final String NR_CSEC_VALIDATOR_HOME_TMP = "/{{NR_CSEC_VALIDATOR_HOME_TMP}}"; public static final String CSEC_IAST_FUZZ_REQUEST_ID = "nr-csec-fuzz-request-id"; @@ -58,10 +61,14 @@ public static K2RequestIdentifier parseFuzzRequestIdentifierHeader(String reques if (data.length >= 7) { for (int i = 6; i < data.length; i++) { String tmpFile = data[i].trim(); + if(StringUtils.contains(tmpFile, NR_CSEC_VALIDATOR_HOME_TMP_URL_ENCODED)) { + tmpFile = urlDecode(tmpFile); + } + tmpFile = StringUtils.replace(tmpFile, NR_CSEC_VALIDATOR_HOME_TMP, + NewRelicSecurity.getAgent().getAgentTempDir()); k2RequestIdentifierInstance.getTempFiles().add(tmpFile); try { - tmpFile = StringUtils.replace(tmpFile, NR_CSEC_VALIDATOR_HOME_TMP, - NewRelicSecurity.getAgent().getAgentTempDir()); + File fileToCreate = new File(tmpFile); if (fileToCreate.getParentFile() != null) { @@ -82,6 +89,23 @@ public static K2RequestIdentifier parseFuzzRequestIdentifierHeader(String reques return k2RequestIdentifierInstance; } + /** + * Method to url decode given encodedString under UTF-8 encoding. If the + * conversion is not possible, original string is returned. + * + * @param encodedString URL encoded string + * @return URL decoded string + */ + public static String urlDecode(String encodedString) { + String decodedString = StringUtils.EMPTY; + try { + decodedString = URLDecoder.decode(encodedString, StandardCharsets.UTF_8.name()); + } catch (Throwable e) { + decodedString = encodedString; + } + return decodedString; + } + public static void registerUserLevelCode(String frameworkName) { try { if (!NewRelicSecurity.isHookProcessingActive() || NewRelicSecurity.getAgent().getSecurityMetaData().getRequest().isEmpty() @@ -92,7 +116,7 @@ public static void registerUserLevelCode(String frameworkName) { if (!securityMetaData.getMetaData().isUserLevelServiceMethodEncountered(frameworkName)) { securityMetaData.getMetaData().setUserLevelServiceMethodEncountered(true); StackTraceElement[] trace = Thread.currentThread().getStackTrace(); - securityMetaData.getMetaData().setServiceTrace(Arrays.copyOfRange(trace, 1, trace.length)); + securityMetaData.getMetaData().setServiceTrace(Arrays.copyOfRange(trace, 2, trace.length)); } } catch (Throwable ignored) { } diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/SystemCommandUtils.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/SystemCommandUtils.java new file mode 100644 index 000000000..aaf7eb03c --- /dev/null +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/instrumentation/helpers/SystemCommandUtils.java @@ -0,0 +1,55 @@ +package com.newrelic.api.agent.security.instrumentation.helpers; + +import com.newrelic.api.agent.security.schema.StringUtils; +import com.newrelic.api.agent.security.schema.operation.ForkExecOperation; + +import java.io.*; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SystemCommandUtils { + + private static final Pattern commandShellRegex = Pattern.compile("(\\S+\\.sh(?!\\S))"); + + public static List isShellScriptExecution(String command) { + Matcher matcher = commandShellRegex.matcher(command); + + List shellScripts = new ArrayList<>(); + while (matcher.find()){ + shellScripts.add(matcher.group().trim()); + } + return shellScripts; + } + + public static List getAbsoluteShellScripts(List shellScripts) { + List absoluteSrcipts = new ArrayList<>(); + + for (String shellScript : shellScripts) { + File script = new File(shellScript); + if(script.isFile()){ + absoluteSrcipts.add(script.getAbsolutePath()); + } + } + + return absoluteSrcipts; + } + + public static void scriptContent(List absolutePaths, ForkExecOperation operation) { + for (String absolutePath : absolutePaths) { + try { + BufferedReader reader = new BufferedReader(new FileReader(absolutePath)); + StringBuilder content = new StringBuilder(); + String line = reader.readLine(); + while(line != null) { + content.append(line); + content.append(StringUtils.LF); + line = reader.readLine(); + } + operation.getScriptContent().put(new File(absolutePath).getName(), content.toString()); + } catch (IOException e) { + } + } + } +} diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/AgentMetaData.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/AgentMetaData.java index 110dc6fbe..ef0dc4cda 100644 --- a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/AgentMetaData.java +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/AgentMetaData.java @@ -38,11 +38,14 @@ public class AgentMetaData { @JsonIgnore private Set ips; + private AppServerInfo appServerInfo; + public AgentMetaData() { this.rciMethodsCalls = new HashSet<>(); this.ips = new HashSet<>(); this.userDataTranslationMap = new HashMap<>(); this.reflectedMetaData = new HashMap<>(); + this.appServerInfo = new AppServerInfo(); } public AgentMetaData(AgentMetaData agentMetaData) { @@ -57,6 +60,7 @@ public AgentMetaData(AgentMetaData agentMetaData) { this.userDataTranslationMap = new HashMap<>(agentMetaData.userDataTranslationMap); this.userLevelServiceMethodEncountered = agentMetaData.userLevelServiceMethodEncountered; this.reflectedMetaData = agentMetaData.reflectedMetaData; + this.appServerInfo = agentMetaData.appServerInfo; } public boolean isTriggerViaRCI() { @@ -151,4 +155,12 @@ public boolean isUserLevelServiceMethodEncountered(String framework) { public void setUserLevelServiceMethodEncountered(boolean userLevelServiceMethodEncountered) { this.userLevelServiceMethodEncountered = userLevelServiceMethodEncountered; } + + public AppServerInfo getAppServerInfo() { + return appServerInfo; + } + + public void setAppServerInfo(AppServerInfo appServerInfo) { + this.appServerInfo = appServerInfo; + } } diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/AppServerInfo.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/AppServerInfo.java new file mode 100644 index 000000000..a04d4255b --- /dev/null +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/AppServerInfo.java @@ -0,0 +1,34 @@ +package com.newrelic.api.agent.security.schema; + +public class AppServerInfo { + + String applicationDirectory; + + String serverBaseDirectory; + + String sameSiteCookies; + + public String getApplicationDirectory() { + return applicationDirectory; + } + + public void setApplicationDirectory(String applicationDirectory) { + this.applicationDirectory = applicationDirectory; + } + + public String getServerBaseDirectory() { + return serverBaseDirectory; + } + + public void setServerBaseDirectory(String serverBaseDirectory) { + this.serverBaseDirectory = serverBaseDirectory; + } + + public String getSameSiteCookies() { + return sameSiteCookies; + } + + public void setSameSiteCookies(String sameSiteCookies) { + this.sameSiteCookies = sameSiteCookies; + } +} diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/StringUtils.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/StringUtils.java index 482fc8409..08cfc96a5 100644 --- a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/StringUtils.java +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/StringUtils.java @@ -2,9 +2,14 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; +import java.util.StringJoiner; +import java.util.Locale; public class StringUtils { public static final String EMPTY = ""; + + public static final String LF = "\n"; public static final int INDEX_NOT_FOUND = -1; /** @@ -446,7 +451,7 @@ private static boolean endsWith(final CharSequence str, final CharSequence suffi * @return whether the region matched */ private static boolean regionMatches(final CharSequence cs, final boolean ignoreCase, final int thisStart, - final CharSequence substring, final int start, final int length) { + final CharSequence substring, final int start, final int length) { if (cs instanceof String && substring instanceof String) { return ((String) cs).regionMatches(ignoreCase, thisStart, (String) substring, start, length); } @@ -831,6 +836,78 @@ public static boolean equalsAny(final CharSequence string, final CharSequence... return false; } + /** + *

Compares two CharSequences, returning {@code true} if they represent + * equal sequences of characters, ignoring case.

+ * + *

{@code null}s are handled without exceptions. Two {@code null} + * references are considered equal. The comparison is case insensitive.

+ * + *
+     * StringUtils.equalsIgnoreCase(null, null)   = true
+     * StringUtils.equalsIgnoreCase(null, "abc")  = false
+     * StringUtils.equalsIgnoreCase("abc", null)  = false
+     * StringUtils.equalsIgnoreCase("abc", "abc") = true
+     * StringUtils.equalsIgnoreCase("abc", "ABC") = true
+     * 
+ * + * @param cs1 the first CharSequence, may be {@code null} + * @param cs2 the second CharSequence, may be {@code null} + * @return {@code true} if the CharSequences are equal (case-insensitive), or both {@code null} + * @since 3.0 Changed signature from equalsIgnoreCase(String, String) to equalsIgnoreCase(CharSequence, CharSequence) + * @see #equals(CharSequence, CharSequence) + */ + public static boolean equalsIgnoreCase(final CharSequence cs1, final CharSequence cs2) { + if (cs1 == cs2) { + return true; + } + if (cs1 == null || cs2 == null) { + return false; + } + if (cs1.length() != cs2.length()) { + return false; + } + return regionMatches(cs1, true, 0, cs2, 0, cs1.length()); + } + + /** + *

Checks if CharSequence contains a search CharSequence irrespective of case, + * handling {@code null}. Case-insensitivity is defined as by + * {@link String#equalsIgnoreCase(String)}. + * + *

A {@code null} CharSequence will return {@code false}.

+ * + *
+     * StringUtils.containsIgnoreCase(null, *) = false
+     * StringUtils.containsIgnoreCase(*, null) = false
+     * StringUtils.containsIgnoreCase("", "") = true
+     * StringUtils.containsIgnoreCase("abc", "") = true
+     * StringUtils.containsIgnoreCase("abc", "a") = true
+     * StringUtils.containsIgnoreCase("abc", "z") = false
+     * StringUtils.containsIgnoreCase("abc", "A") = true
+     * StringUtils.containsIgnoreCase("abc", "Z") = false
+     * 
+ * + * @param str the CharSequence to check, may be null + * @param searchStr the CharSequence to find, may be null + * @return true if the CharSequence contains the search CharSequence irrespective of + * case or false if not or {@code null} string input + * @since 3.0 Changed signature from containsIgnoreCase(String, String) to containsIgnoreCase(CharSequence, CharSequence) + */ + public static boolean containsIgnoreCase(final CharSequence str, final CharSequence searchStr) { + if (str == null || searchStr == null) { + return false; + } + final int len = searchStr.length(); + final int max = str.length() - len; + for (int i = 0; i <= max; i++) { + if (regionMatches(str, true, i, searchStr, 0, len)) { + return true; + } + } + return false; + } + /** *

Check if a CharSequence starts with a specified prefix (optionally case insensitive).

* @@ -938,4 +1015,165 @@ public static boolean startsWithIgnoreCase(final CharSequence str, final CharSeq return startsWith(str, prefix, true); } + /** + *

Converts a String to lower case as per {@link String#toLowerCase()}.

+ * + *

A {@code null} input String returns {@code null}.

+ * + *
+     * StringUtils.lowerCase(null)  = null
+     * StringUtils.lowerCase("")    = ""
+     * StringUtils.lowerCase("aBc") = "abc"
+     * 
+ * + *

Note: As described in the documentation for {@link String#toLowerCase()}, + * the result of this method is affected by the current locale. + * should be used with a specific locale (e.g. {@link Locale#ENGLISH}).

+ * + * @param str the String to lower case, may be null + * @return the lower cased String, {@code null} if null String input + */ + public static String lowerCase(final String str) { + if (str == null) { + return null; + } + return str.toLowerCase(); + } + + /** + *

Joins the elements of the provided array into a single String + * containing the provided list of elements.

+ * + *

No delimiter is added before or after the list. + * A {@code null} separator is the same as an empty String (""). + * Null objects or empty strings within the array are represented by + * empty strings.

+ * + *
+     * StringUtils.join(null, *, *, *)                = null
+     * StringUtils.join([], *, *, *)                  = ""
+     * StringUtils.join([null], *, *, *)              = ""
+     * StringUtils.join(["a", "b", "c"], "--", 0, 3)  = "a--b--c"
+     * StringUtils.join(["a", "b", "c"], "--", 1, 3)  = "b--c"
+     * StringUtils.join(["a", "b", "c"], "--", 2, 3)  = "c"
+     * StringUtils.join(["a", "b", "c"], "--", 2, 2)  = ""
+     * StringUtils.join(["a", "b", "c"], null, 0, 3)  = "abc"
+     * StringUtils.join(["a", "b", "c"], "", 0, 3)    = "abc"
+     * StringUtils.join([null, "", "a"], ',', 0, 3)   = ",,a"
+     * 
+ * + * @param array the array of values to join together, may be null + * @param delimiter the separator character to use, null treated as "" + * @param startIndex the first index to start joining from. + * @param endIndex the index to stop joining from (exclusive). + * @return the joined String, {@code null} if null array input; or the empty string + * if {@code endIndex - startIndex <= 0}. The number of joined entries is given by + * {@code endIndex - startIndex} + * @throws ArrayIndexOutOfBoundsException ife
+ * {@code startIndex < 0} or
+ * {@code startIndex >= array.length()} or
+ * {@code endIndex < 0} or
+ * {@code endIndex > array.length()} + */ + public static String join(final Object[] array, final String delimiter, final int startIndex, final int endIndex) { + if (array == null) { + return null; + } + if (endIndex - startIndex <= 0) { + return EMPTY; + } + final StringJoiner joiner = new StringJoiner(toStringOrEmpty(delimiter)); + for (int i = startIndex; i < endIndex; i++) { + joiner.add(toStringOrEmpty(array[i])); + } + return joiner.toString(); + } + + /** + *

Joins the elements of the provided array into a single String + * containing the provided list of elements.

+ * + *

No separator is added to the joined String. + * Null objects or empty strings within the array are represented by + * empty strings.

+ * + *
+     * StringUtils.join(null)            = null
+     * StringUtils.join([])              = ""
+     * StringUtils.join([null])          = ""
+     * StringUtils.join(["a", "b", "c"]) = "abc"
+     * StringUtils.join([null, "", "a"]) = "a"
+     * 
+ * + * @param the specific type of values to join together + * @param elements the values to join together, may be null + * @return the joined String, {@code null} if null array input + * @since 2.0 + * @since 3.0 Changed signature to use varargs + */ + @SafeVarargs + public static String join(final T... elements) { + return join(elements, null, 0, elements.length); + } + + private static String toStringOrEmpty(final Object obj) { + return Objects.toString(obj, EMPTY); + } + + /** + *

Checks if CharSequence contains a search CharSequence, handling {@code null}. + * This method uses {@link String#indexOf(String)} if possible.

+ * + *

A {@code null} CharSequence will return {@code false}.

+ * + *
+     * StringUtils.contains(null, *)     = false
+     * StringUtils.contains(*, null)     = false
+     * StringUtils.contains("", "")      = true
+     * StringUtils.contains("abc", "")   = true
+     * StringUtils.contains("abc", "a")  = true
+     * StringUtils.contains("abc", "z")  = false
+     * 
+ * + * @param seq the CharSequence to check, may be null + * @param searchSeq the CharSequence to find, may be null + * @return true if the CharSequence contains the search CharSequence, + * false if not or {@code null} string input + * @since 2.0 + * @since 3.0 Changed signature from contains(String, String) to contains(CharSequence, CharSequence) + */ + public static boolean contains(final CharSequence seq, final CharSequence searchSeq) { + if (seq == null || searchSeq == null) { + return false; + } + return indexOf(seq, searchSeq, 0) >= 0; + } + + /** + * Used by the indexOf(CharSequence methods) as a green implementation of indexOf. + * + * @param cs the {@code CharSequence} to be processed + * @param searchChar the {@code CharSequence} to be searched for + * @param start the start index + * @return the index where the search sequence was found + */ + static int indexOf(final CharSequence cs, final CharSequence searchChar, final int start) { + if (cs instanceof String) { + return ((String) cs).indexOf(searchChar.toString(), start); + } else if (cs instanceof StringBuilder) { + return ((StringBuilder) cs).indexOf(searchChar.toString(), start); + } else if (cs instanceof StringBuffer) { + return ((StringBuffer) cs).indexOf(searchChar.toString(), start); + } + return cs.toString().indexOf(searchChar.toString(), start); +// if (cs instanceof String && searchChar instanceof String) { +// // TODO: Do we assume searchChar is usually relatively small; +// // If so then calling toString() on it is better than reverting to +// // the green implementation in the else block +// return ((String) cs).indexOf((String) searchChar, start); +// } else { +// // TODO: Implement rather than convert to String +// return cs.toString().indexOf(searchChar.toString(), start); +// } + } } diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/VulnerabilityCaseType.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/VulnerabilityCaseType.java index f51be2edc..f347414c6 100644 --- a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/VulnerabilityCaseType.java +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/VulnerabilityCaseType.java @@ -68,7 +68,9 @@ public enum VulnerabilityCaseType { /** JavaScript Injection */ JAVASCRIPT_INJECTION("JAVASCRIPT_INJECTION"), /** JavaScript Injection */ - XQUERY_INJECTION("XQUERY_INJECTION"); + XQUERY_INJECTION("XQUERY_INJECTION"), + + CACHING_DATA_STORE("CACHING_DATA_STORE"); /** case type. */ diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/helper/RedisCommands.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/helper/RedisCommands.java new file mode 100644 index 000000000..d9f17958e --- /dev/null +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/helper/RedisCommands.java @@ -0,0 +1,230 @@ +package com.newrelic.api.agent.security.schema.helper; + +import java.util.Arrays; +import java.util.List; + +public class RedisCommands { + + public static final List readCommands = Arrays.asList( + "ACL", + "BITCOUNT", + "BITPO", + "CONFIG", + "DBSIZE", + "DUMP", + "ECHO", + "EVAL", + "EVAL_RO", + "EVALSHA", + "EVALSHA_RO", + "EXISTS", + "EXPIRETIME", + "GEODIST", + "GEOHASH", + "GEOPOS", + "GEORADIUS", + "GEORADIUS_RO", + "GEORADIUSBYMEMBER", + "GEORADIUSBYMEMBER_RO", + "GEOSEARCH", + "GET", + "GETBIT", + "GETDEL", + "GETEX", + "GETRANGE", + "GETSET", + "HEXISTS", + "HGET", + "HGETALL", + "HKEYS", + "HLEN", + "HMGET", + "HRANDFIELD", + "HSCAN", + "HSTRLEN", + "HVALS", + "INFO", + "KEYS", + "LINDEX", + "LLEN", + "LPOS", + "LRANGE", + "MEMORY", + "MGET", + "MODULE LIST", + "OBJECT", + "PFCOUNT", + "PTTL", + "RANDOMKEY", + "SCAN", + "SCARD", + "SCRIPT", + "SDIFF", + "SDIFFSTORE", + "SINTER", + "SINTERCARD", + "SINTERSTORE", + "SISMEMBER", + "SLOWLOG", + "SMEMBERS", + "SMISMEMBER", + "SORT", + "SORT_RO", + "SRANDMEMBER", + "SSCAN", + "STRLEN", + "SUNION", + "SUNIONSTORE", + "TOUCH", + "TTL", + "TYPE", + "XACK", + "XINFO", + "XLEN", + "XPENDING", + "XRANGE", + "XREAD", + "XREADGROUP", + "XREVRANGE", + "ZCARD", + "ZCOUNT", + "ZDIFF", + "ZDIFFSTORE", + "ZINTER", + "ZINTERCARD", + "ZLEXCOUNT", + "ZMSCORE", + "ZRANDMEMBER", + "ZRANGE", + "ZRANGEBYLEX", + "ZRANGEBYSCORE", + "ZRANGESTORE", + "ZRANK", + "ZREVRANGE", + "ZREVRANGEBYLEX", + "ZREVRANGEBYSCORE", + "ZREVRANK", + "ZSCAN", + "ZSCORE", + "ZUNION", + "ZUNIONSTORE" + ); + + public static final List writeCommands = Arrays.asList( + "ACL", + "APPEND", + "BITPOS", + "BGREWRITEAOF", + "BITFIELD", + "BITOP", + "BLMOVE", + "BRPOPLPUSH", + "CONFIG", + "COPY", + "DECR", + "DECRBY", + "EVAL", + "EVALSHA", + "EXPIRE", + "EXPIREAT", + "GEOADD", + "GEOSEARCHSTORE", + "GETEX", + "GETSET", + "HINCRBY", + "HINCRBYFLOAT", + "HMSET", + "HSET", + "HSETNX", + "INCR", + "INCRBY", + "INCRBYFLOAT", + "LINSERT", + "LMOVE", + "LPUSH", + "LPUSHX", + "LSET", + "MIGRATE", + "MOVE", + "MSET", + "MSETNX", + "PERSIST", + "PEXPIRE", + "PEXPIREAT", + "PEXPIRETIME", + "PFADD", + "PFMERGE", + "PSETEX", + "RENAME", + "RENAMENX", + "RESTORE", + "RPOPLPUSH", + "RPUSH", + "RPUSHX", + "SADD", + "SCRIPT DEBUG", + "SDIFFSTORE", + "SET", + "SETEX", + "SETBIT", + "SETNX", + "SETRANGE", + "SINTERSTORE", + "SMOVE", + "SUNIONSTORE", + "SWAPDB", + "XADD", + "XAUTOCLAIM", + "XCLAIM", + "XGROUP", + "ZADD", + "ZDIFFSTORE", + "ZINCRBY", + "ZINTERSTORE", + "ZRANGESTORE", + "ZUNIONSTORE" + ); + + public static final List deleteCommands = Arrays.asList( + "ACL", + "BLMPOP", + "BLPOP", + "BRPOP", + "BRPOPLPUSH", + "BZPOPMAX", + "BZPOPMIN", + "DEL", + "DISCARD", + "EVAL", + "EVALSHA", + "FLUSHALL", + "FLUSHDB", + "GETDEL", + "HDEL", + "LMPOP", + "LPOP", + "LREM", + "MEMORY", + "RPOP", + "RPOPLPUSH", + "SCRIPT", + "SLOWLOG", + "SPOP", + "SREM", + "UNLINK", + "LTRIM", + "XDEL", + "XGROUP ", + "XTRIM", + "ZPOPMAX", + "ZPOPMIN", + "ZREM", + "ZREMRANGEBYLEX", + "ZREMRANGEBYRANK", + "ZREMRANGEBYSCORE" + ); + + public static final String READ_COMMAND = "READ"; + public static final String WRITE_COMMAND = "WRITE"; + public static final String DELETE_COMMAND = "DELETE" ; +} diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/FileIntegrityOperation.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/FileIntegrityOperation.java index 21a20a258..b73e0c740 100644 --- a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/FileIntegrityOperation.java +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/FileIntegrityOperation.java @@ -1,8 +1,17 @@ package com.newrelic.api.agent.security.schema.operation; import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.StringUtils; import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFileAttributes; +import java.nio.file.attribute.PosixFilePermission; +import java.util.Set; + public class FileIntegrityOperation extends AbstractOperation { private Boolean exists; @@ -12,11 +21,20 @@ public class FileIntegrityOperation extends AbstractOperation { private Integer lineNumber; private String fileName; - public FileIntegrityOperation(Boolean exists, String fileName, String className, String methodName) { + private Long lastModified; + + private String permissionString; + + private Long length; + + public FileIntegrityOperation(Boolean exists, String fileName, String className, String methodName, Long lastModified, String permissionString, Long length) { super(className, methodName); this.setCaseType(VulnerabilityCaseType.FILE_INTEGRITY); this.exists = exists; this.setFileName(fileName); + this.lastModified = lastModified; + this.permissionString = permissionString; + this.length = length; } /** @@ -89,14 +107,6 @@ public void setLineNumber(Integer lineNumber) { this.lineNumber = lineNumber; } - public void setBeanValues(String methodName, String userFileName, String userMethodName, String currentMethod, - Integer lineNumber) { - this.userFileName = userFileName; - this.userMethodName = userMethodName; - this.currentMethod = currentMethod; - this.lineNumber = lineNumber; - } - @Override public boolean isEmpty() { return (fileName == null || fileName.trim().isEmpty()); @@ -115,4 +125,36 @@ public String getFileName() { public void setFileName(String fileName) { this.fileName = fileName; } + + public Long getLastModified() { + return lastModified; + } + + public void setLastModified(Long lastModified) { + this.lastModified = lastModified; + } + + public String getPermissionString() { + return permissionString; + } + + public void setPermissionString(String permissionString) { + this.permissionString = permissionString; + } + + public boolean isIntegrityBreached(File file){ + Boolean exists = file.exists(); + long lastModified = exists? file.lastModified() : -1; + String permissions = StringUtils.EMPTY; + long length = file.length(); + try { + if(exists) { + PosixFileAttributes fileAttributes = Files.readAttributes(Paths.get(file.getPath()), PosixFileAttributes.class); + Set permissionSet = fileAttributes.permissions(); + permissions = permissionSet.toString(); + } + } catch (IOException e) { + } + return (exists != this.exists || lastModified != this.lastModified || !StringUtils.equals(permissions, this.permissionString) || length != this.length); + } } diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/ForkExecOperation.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/ForkExecOperation.java index ad6b918b4..0ddd76bca 100644 --- a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/ForkExecOperation.java +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/ForkExecOperation.java @@ -12,6 +12,8 @@ public class ForkExecOperation extends AbstractOperation { private Map environment; + private Map scriptContent = new HashMap<>(); + public ForkExecOperation(String cmd, Map environment, String className, String methodName) { super(className, methodName); this.setCaseType(VulnerabilityCaseType.SYSTEM_COMMAND); @@ -50,4 +52,11 @@ public void setEnvironment(Map environment) { this.environment = environment; } + public Map getScriptContent() { + return scriptContent; + } + + public void setScriptContent(Map scriptContent) { + this.scriptContent = scriptContent; + } } diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/JCacheOperation.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/JCacheOperation.java new file mode 100644 index 000000000..f9b3c9f71 --- /dev/null +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/JCacheOperation.java @@ -0,0 +1,50 @@ +package com.newrelic.api.agent.security.schema.operation; + +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; +import com.newrelic.api.agent.security.schema.helper.RedisCommands; + +import java.util.List; + +public class JCacheOperation extends AbstractOperation { + public static final String JCACHE = "JCACHE"; + private String type; + + private List arguments; + + private String category; + + public JCacheOperation(String className, String methodName, String type, List arguments) { + super(className, methodName); + this.setCaseType(VulnerabilityCaseType.CACHING_DATA_STORE); + this.category = JCACHE; + this.type = type; + this.arguments = arguments; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public List getArguments() { + return arguments; + } + + public void setArguments(List arguments) { + this.arguments = arguments; + } + + public String getCategory() { + return category; + } + + @Override + public boolean isEmpty() { + return (type == null || type.trim().isEmpty()); + } + +} diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/MemcachedOperation.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/MemcachedOperation.java new file mode 100644 index 000000000..be1c90b51 --- /dev/null +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/MemcachedOperation.java @@ -0,0 +1,69 @@ +package com.newrelic.api.agent.security.schema.operation; + +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; + +import java.util.List; + +public class MemcachedOperation extends AbstractOperation { + public static String MEMCACHED = "MEMCACHED"; + private String type; + + private List arguments; + + private String category; + + private String command; + + public MemcachedOperation(String command, List arguments, String type, String className, String methodName) { + super(className, methodName); + this.setCaseType(VulnerabilityCaseType.CACHING_DATA_STORE); + this.arguments = arguments; + this.type = type; + this.command = command; + this.category = MEMCACHED; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public List getArguments() { + return arguments; + } + + public void setArguments(List arguments) { + this.arguments = arguments; + } + + public String getCommand() { + return command; + } + + public void setCommand(String command) { + this.command = command; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + @Override + public boolean isEmpty() { + return arguments == null || arguments.isEmpty(); + } + + @Override + public String toString() { + return "arguments: " + arguments + "; type: " + type + "; command: " + command; + } + +} diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/RedisOperation.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/RedisOperation.java new file mode 100644 index 000000000..a67731dab --- /dev/null +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/RedisOperation.java @@ -0,0 +1,71 @@ +package com.newrelic.api.agent.security.schema.operation; + +import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; +import com.newrelic.api.agent.security.schema.helper.RedisCommands; + +import java.util.List; + +public class RedisOperation extends AbstractOperation { + public static final String REDIS = "REDIS"; + private String type; + + private List arguments; + + private String category; + + private String mode; + + public RedisOperation(String className, String methodName, String type, List arguments) { + super(className, methodName); + this.setCaseType(VulnerabilityCaseType.CACHING_DATA_STORE); + this.category = REDIS; + this.type = type; + this.arguments = arguments; + this.mode = getMode(type); + } + + private String getMode(String type) { + if(RedisCommands.writeCommands.contains(type)){ + return RedisCommands.WRITE_COMMAND; + } else if (RedisCommands.deleteCommands.contains(type)) { + return RedisCommands.DELETE_COMMAND; + } else { + return RedisCommands.READ_COMMAND; + } + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public List getArguments() { + return arguments; + } + + public void setArguments(List arguments) { + this.arguments = arguments; + } + + public String getCategory() { + return category; + } + + public String getMode() { + return mode; + } + + public void setMode(String mode) { + this.mode = mode; + } + + @Override + public boolean isEmpty() { + return (type == null || type.trim().isEmpty()); + } + +} diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/SQLOperation.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/SQLOperation.java index 6e3d5a66e..aab96090d 100644 --- a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/SQLOperation.java +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/SQLOperation.java @@ -17,6 +17,7 @@ public class SQLOperation extends AbstractOperation { private String dbName = "UNKNOWN"; private boolean isPreparedCall; + private boolean isStoredProcedureCall; public SQLOperation(String className, String methodName) { super(className, methodName); @@ -91,5 +92,13 @@ public void setDbName(String dbName) { this.dbName = dbName; } } + + public boolean isStoredProcedureCall() { + return isStoredProcedureCall; + } + + public void setStoredProcedureCall(boolean storedProcedureCall) { + isStoredProcedureCall = storedProcedureCall; + } } diff --git a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/SecureCookieOperation.java b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/SecureCookieOperation.java index f8ce9cb21..8b381dc10 100644 --- a/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/SecureCookieOperation.java +++ b/newrelic-security-api/src/main/java/com/newrelic/api/agent/security/schema/operation/SecureCookieOperation.java @@ -1,17 +1,32 @@ package com.newrelic.api.agent.security.schema.operation; import com.newrelic.api.agent.security.schema.AbstractOperation; +import com.newrelic.api.agent.security.schema.StringUtils; import com.newrelic.api.agent.security.schema.VulnerabilityCaseType; public class SecureCookieOperation extends AbstractOperation { private String value; + private boolean isSecure; + private boolean isHttpOnly; + private boolean isSameSiteStrict; + + private String cookie; + public SecureCookieOperation(String value, String className, String methodName) { super(className, methodName); this.setCaseType(VulnerabilityCaseType.SECURE_COOKIE); this.value = value; } + public SecureCookieOperation(String value, boolean isSecure, boolean isHttpOnly, boolean isSameSiteStrict, String cookie, String className, String methodName) { + this(value, className, methodName); + this.isSecure = isSecure; + this.isHttpOnly = isHttpOnly; + this.isSameSiteStrict = isSameSiteStrict; + this.cookie = cookie; + } + public String getValue() { return value; } @@ -20,6 +35,38 @@ public void setValue(String value) { this.value = value; } + public boolean isSecure() { + return isSecure; + } + + public void setSecure(boolean secure) { + isSecure = secure; + } + + public boolean isHttpOnly() { + return isHttpOnly; + } + + public void setHttpOnly(boolean httpOnly) { + isHttpOnly = httpOnly; + } + + public boolean isSameSiteStrict() { + return isSameSiteStrict; + } + + public void setSameSiteStrict(boolean sameSiteStrict) { + isSameSiteStrict = sameSiteStrict; + } + + public String getCookie() { + return cookie; + } + + public void setCookie(String cookie) { + this.cookie = cookie; + } + @Override public boolean isEmpty() { return (value == null || value.trim().isEmpty()); diff --git a/settings.gradle b/settings.gradle index b16397fc4..c909c49cb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -50,6 +50,7 @@ include 'instrumentation:jsp-3' include 'instrumentation:urlconnection' include 'instrumentation:httpclient-3' include 'instrumentation:httpclient-4.0' +include 'instrumentation:httpclient-5.0' include 'instrumentation:httpclient-jdk11' include 'instrumentation:jdbc-generic' include 'instrumentation:jdbc-db2' @@ -115,7 +116,7 @@ include 'instrumentation:akka-http-core-2.11_10.0.11' include 'instrumentation:akka-http-core-10.0' include 'instrumentation:jetty-9' include 'instrumentation:jetty-11' -include 'instrumentation:netty-4.0.0' +//include 'instrumentation:netty-4.0.0' //include 'instrumentation:grpc-1.40.0' include 'instrumentation:dynamodb-1.11.80' include 'instrumentation:dynamodb-1.11.390' @@ -152,4 +153,16 @@ include 'instrumentation:commons-jxpath' //include 'instrumentation:apache-wicket-6.4' //include 'instrumentation:apache-wicket-7.0' //include 'instrumentation:apache-wicket-8.0' -include 'instrumentation:ning-async-http-client-1.0.0' \ No newline at end of file +include 'instrumentation:ning-async-http-client-1.0.0' +include 'instrumentation:jersey-2' +include 'instrumentation:jersey-2.16' +include 'instrumentation:spring-data-redis' +include 'instrumentation:jcache-1.0.0' +include 'instrumentation:lettuce-4.3' +include 'instrumentation:lettuce-5.0' +include 'instrumentation:spymemcached-2.12.0' +include 'instrumentation:jetty-12' +include 'instrumentation:async-http-client-2.0.0' +include 'instrumentation:sun-net-httpserver' +include 'instrumentation:tomcat-7' +include 'instrumentation:tomcat-8'