diff --git a/instrumentation-security/http4s-ember-client-2.12_0.23/build.gradle b/instrumentation-security/http4s-ember-client-2.12_0.23/build.gradle new file mode 100644 index 000000000..1bb37776e --- /dev/null +++ b/instrumentation-security/http4s-ember-client-2.12_0.23/build.gradle @@ -0,0 +1,27 @@ +apply plugin: 'scala' + +isScalaProjectEnabled(project, "scala-2.12") + +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.scala-lang:scala-library:2.12.14") + implementation('org.http4s:http4s-ember-client_2.12:0.23.12') + implementation("org.typelevel:cats-effect_2.12:3.3.0") + testImplementation("org.http4s:http4s-dsl_2.12:0.23.12") + +} + +jar { + manifest { + attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.http4s-ember-client-2.12_0.23' + } +} +verifyInstrumentation { + passes 'org.http4s:http4s-ember-client_2.12:[0.23.0,0.24.0)' + excludeRegex '.*(RC|M)[0-9]*' +} + +sourceSets.main.scala.srcDirs = ['src/main/scala', 'src/main/java'] +sourceSets.main.java.srcDirs = [] diff --git a/instrumentation-security/http4s-ember-client-2.12_0.23/src/main/java/org/http4s/EmberClientBuilder_Instrumentation.java b/instrumentation-security/http4s-ember-client-2.12_0.23/src/main/java/org/http4s/EmberClientBuilder_Instrumentation.java new file mode 100644 index 000000000..5e56dfaa3 --- /dev/null +++ b/instrumentation-security/http4s-ember-client-2.12_0.23/src/main/java/org/http4s/EmberClientBuilder_Instrumentation.java @@ -0,0 +1,20 @@ +package org.http4s; + +import cats.effect.kernel.Async; +import cats.effect.kernel.Resource; +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.http4s.ember.NewrelicSecurityClientMiddleware$; +import org.http4s.client.Client; + +@Weave(type = MatchType.ExactClass, originalName = "org.http4s.ember.client.EmberClientBuilder") +public abstract class EmberClientBuilder_Instrumentation { + + private final Async evidence$1 = Weaver.callOriginal(); + + public Resource> build() { + Resource> delegateResource = Weaver.callOriginal(); + return NewrelicSecurityClientMiddleware$.MODULE$.resource(delegateResource, evidence$1); + } +} diff --git a/instrumentation-security/http4s-ember-client-2.12_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/NewrelicSecurityClientMiddleware.scala b/instrumentation-security/http4s-ember-client-2.12_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/NewrelicSecurityClientMiddleware.scala new file mode 100644 index 000000000..594241802 --- /dev/null +++ b/instrumentation-security/http4s-ember-client-2.12_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/NewrelicSecurityClientMiddleware.scala @@ -0,0 +1,125 @@ +package com.newrelic.agent.security.instrumentation.http4s.ember + +import cats.effect.kernel.Async +import cats.effect.{Resource, Sync} +import com.newrelic.api.agent.security.NewRelicSecurity +import com.newrelic.api.agent.security.instrumentation.helpers.{GenericHelper, ServletHelper} +import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityException +import com.newrelic.api.agent.security.schema.operation.SSRFOperation +import com.newrelic.api.agent.security.schema.{AbstractOperation, StringUtils, VulnerabilityCaseType} +import com.newrelic.api.agent.security.utils.SSRFUtils +import com.newrelic.api.agent.security.utils.logging.LogLevel +import org.http4s.Request +import org.http4s.client.Client + +import java.net.URI + +object NewrelicSecurityClientMiddleware { + private final val nrSecCustomAttrName: String = "HTTP4S-EMBER-CLIENT-OUTBOUND" + private final val HTTP4S_EMBER_CLIENT: String = "HTTP4S-EMBER-CLIENT-2.12_0.23" + + private def construct[F[_] : Sync, T](t: T): F[T] = Sync[F].delay(t) + + private def clientResource[F[_] : Async](client: Client[F]): Client[F] = + Client { req: Request[F] => + for { + // pre-process hook + operation <- Resource.eval( + construct { + val isLockAcquired = GenericHelper.acquireLockIfPossible(VulnerabilityCaseType.HTTP_REQUEST, nrSecCustomAttrName) + var operation: AbstractOperation = null + if (isLockAcquired) { + operation = preprocessSecurityHook(req) + } + operation + }) + // add Security Headers + request <- Resource.eval(construct {addSecurityHeaders(req, operation)}) + + // original call + response <- client.run(request) + + // post process and register exit event + newRes <- Resource.eval(construct{ + val isLockAcquired = GenericHelper.isLockAcquired(nrSecCustomAttrName); + if (isLockAcquired) { + GenericHelper.releaseLock(nrSecCustomAttrName) + } + registerExitOperation(isLockAcquired, operation) + response + }) + + } yield newRes + } + + def resource[F[_] : Async](delegate: Resource[F, Client[F]]): Resource[F, Client[F]] = { + val res: Resource[F, Client[F]] = delegate.map(c =>clientResource(c)) + res + } + + + private def preprocessSecurityHook[F[_] : Async](httpRequest: Request[F]): AbstractOperation = { + try { + val securityMetaData = NewRelicSecurity.getAgent.getSecurityMetaData + if (!NewRelicSecurity.isHookProcessingActive || securityMetaData.getRequest.isEmpty) return null + // Generate required URL + var methodURI: URI = null + var uri: String = null + try { + methodURI = new URI(httpRequest.uri.toString) + uri = methodURI.toString + if (methodURI == null) return null + } catch { + case ignored: Exception => + NewRelicSecurity.getAgent.log(LogLevel.WARNING, String.format(GenericHelper.URI_EXCEPTION_MESSAGE, HTTP4S_EMBER_CLIENT, ignored.getMessage), ignored, this.getClass.getName) + return null + } + return new SSRFOperation(uri, this.getClass.getName, "run") + } catch { + case e: Throwable => + if (e.isInstanceOf[NewRelicSecurityException]) { + NewRelicSecurity.getAgent.log(LogLevel.WARNING, String.format(GenericHelper.SECURITY_EXCEPTION_MESSAGE, HTTP4S_EMBER_CLIENT, e.getMessage), e, this.getClass.getName) + throw e + } + NewRelicSecurity.getAgent.log(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP4S_EMBER_CLIENT, e.getMessage), e, this.getClass.getName) + NewRelicSecurity.getAgent.reportIncident(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP4S_EMBER_CLIENT, e.getMessage), e, this.getClass.getName) + } + null + } + + private def addSecurityHeaders[F[_] : Async](request: Request[F], operation: AbstractOperation): Request[F] = { + val outboundRequest = new OutboundRequest(request) + if (operation != null) { + val securityMetaData = NewRelicSecurity.getAgent.getSecurityMetaData + val iastHeader = NewRelicSecurity.getAgent.getSecurityMetaData.getFuzzRequestIdentifier.getRaw + if (iastHeader != null && !iastHeader.trim.isEmpty) { + outboundRequest.setHeader(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID, iastHeader) + } + val csecParentId = securityMetaData.getCustomAttribute(GenericHelper.CSEC_PARENT_ID, classOf[String]) + if (StringUtils.isNotBlank(csecParentId)) { + outboundRequest.setHeader(GenericHelper.CSEC_PARENT_ID, csecParentId) + } + try { + NewRelicSecurity.getAgent.getSecurityMetaData.getMetaData.setFromJumpRequiredInStackTrace(Integer.valueOf(4)) + NewRelicSecurity.getAgent.registerOperation(operation) + } + finally { + if (operation.getApiID != null && !operation.getApiID.trim.isEmpty && operation.getExecutionId != null && !operation.getExecutionId.trim.isEmpty) { + outboundRequest.setHeader(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER, SSRFUtils.generateTracingHeaderValue(securityMetaData.getTracingHeaderValue, operation.getApiID, operation.getExecutionId, NewRelicSecurity.getAgent.getAgentUUID)) + } + } + } + outboundRequest.getRequest + } + + private def registerExitOperation(isProcessingAllowed: Boolean, operation: AbstractOperation): Unit = { + try { + if (operation == null || !isProcessingAllowed || !NewRelicSecurity.isHookProcessingActive || NewRelicSecurity.getAgent.getSecurityMetaData.getRequest.isEmpty) return + NewRelicSecurity.getAgent.registerExitEvent(operation) + } catch { + case e: Throwable => + NewRelicSecurity.getAgent.log(LogLevel.FINEST, String.format(GenericHelper.EXIT_OPERATION_EXCEPTION_MESSAGE, HTTP4S_EMBER_CLIENT, e.getMessage), e, this.getClass.getName) + } + } +} + diff --git a/instrumentation-security/http4s-ember-client-2.12_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/OutboundRequest.scala b/instrumentation-security/http4s-ember-client-2.12_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/OutboundRequest.scala new file mode 100644 index 000000000..ca6b52a95 --- /dev/null +++ b/instrumentation-security/http4s-ember-client-2.12_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/OutboundRequest.scala @@ -0,0 +1,19 @@ +package com.newrelic.agent.security.instrumentation.http4s.ember + +import org.http4s.{Header, Request} +import org.typelevel.ci.CIString + +/** + * Http4s's HttpRequest is immutable so we have to create a copy with the new headers. + */ + +class OutboundRequest[F[_]](request: Request[F]) { + private var req: Request[F] = request + + def setHeader(key: String, value: String): Unit = { + req = req.withHeaders(req.headers.put(Header.Raw.apply(CIString.apply(key), value))) + } + def getRequest: Request[F] = { + req + } +} \ No newline at end of file diff --git a/instrumentation-security/http4s-ember-client-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/EmberClientTest.scala b/instrumentation-security/http4s-ember-client-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/EmberClientTest.scala new file mode 100644 index 000000000..ccb26c514 --- /dev/null +++ b/instrumentation-security/http4s-ember-client-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/EmberClientTest.scala @@ -0,0 +1,87 @@ +package com.nr.agent.security.instrumentation.blaze.client + +import cats.effect.unsafe.implicits.global +import cats.effect.{Async, IO} +import com.newrelic.agent.security.introspec.internal.HttpServerRule +import com.newrelic.agent.security.introspec.{InstrumentationTestConfig, SecurityInstrumentationTestRunner, SecurityIntrospector} +import com.newrelic.api.agent.security.instrumentation.helpers.{GenericHelper, ServletHelper} +import com.newrelic.api.agent.security.schema.operation.SSRFOperation +import com.newrelic.api.agent.security.schema.{AbstractOperation, VulnerabilityCaseType} +import com.nr.agent.security.instrumentation.blaze.client.Http4sTestUtils.makeRequest +import org.http4s.ember.client.EmberClientBuilder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.{Assert, FixMethodOrder, Rule, Test} + +import java.util +import java.util.UUID +import scala.concurrent.duration.DurationInt + +@RunWith(classOf[SecurityInstrumentationTestRunner]) +@InstrumentationTestConfig(includePrefixes = Array("org.http4s", "com.newrelic.agent.security.instrumentation.http4s")) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class EmberClientTest { + + @Rule + def server: HttpServerRule = httpServer + + val httpServer = new HttpServerRule() + + @Test + def blazeClientTest(): Unit = { + + val introspector: SecurityIntrospector = SecurityInstrumentationTestRunner.getIntrospector + makeRequest[IO](s"${server.getEndPoint}").unsafeRunTimed(2.seconds) + assertSSRFOperation(introspector.getOperations) + } + + @Test + def blazeClientTestWithHeaders(): Unit = { + val headerValue = String.valueOf(UUID.randomUUID) + + val introspector: SecurityIntrospector = SecurityInstrumentationTestRunner.getIntrospector + setCSECHeaders(headerValue = headerValue, introspector = introspector) + makeRequest[IO](s"${server.getEndPoint}").unsafeRunTimed(2.seconds) + assertSSRFOperation(introspector.getOperations) + verifyHeaders(headerValue, httpServer.getHeaders) + } + + + private def assertSSRFOperation(operations: util.List[AbstractOperation]): Unit = { + Assert.assertTrue("Incorrect number of operations detected!", operations.size == 1) + Assert.assertTrue("SSRFOperation not found!", operations.get(0).isInstanceOf[SSRFOperation]) + val operation: SSRFOperation = operations.get(0).asInstanceOf[SSRFOperation] + + Assert.assertFalse("operation should not be empty", operation.isEmpty) + Assert.assertFalse("JNDILookup should be false", operation.isJNDILookup) + Assert.assertFalse("LowSeverityHook should be disabled", operation.isLowSeverityHook) + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType) + Assert.assertEquals("Invalid executed method name.", "run", operation.getMethodName) + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint.toString, operation.getArg) + } + + private def verifyHeaders(headerValue: String, headers: util.Map[String, String]): Unit = { + 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 + "a", headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)) + Assert.assertTrue(String.format("Missing CSEC header: %s", GenericHelper.CSEC_PARENT_ID), headers.containsKey(GenericHelper.CSEC_PARENT_ID)) + Assert.assertEquals(String.format("Invalid CSEC header value for: %s", GenericHelper.CSEC_PARENT_ID), headerValue + "b", headers.get(GenericHelper.CSEC_PARENT_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)) + } + + private def setCSECHeaders(headerValue: String, introspector: SecurityIntrospector): Unit = { + introspector.setK2FuzzRequestId(headerValue + "a") + introspector.setK2ParentId(headerValue + "b") + introspector.setK2TracingData(headerValue) + } +} + +object Http4sTestUtils { + def makeRequest[F[_]: Async](url: String): F[String] = { + val client = EmberClientBuilder.default[F].build + client.use { c => + c.expect[String](url) + } + } +} + diff --git a/instrumentation-security/http4s-ember-client-2.13_0.23/build.gradle b/instrumentation-security/http4s-ember-client-2.13_0.23/build.gradle new file mode 100644 index 000000000..88b683f7b --- /dev/null +++ b/instrumentation-security/http4s-ember-client-2.13_0.23/build.gradle @@ -0,0 +1,27 @@ +apply plugin: 'scala' + +isScalaProjectEnabled(project, "scala-2.13") + +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.scala-lang:scala-library:2.13.3") + implementation('org.http4s:http4s-ember-client_2.13:0.23.12') + implementation("org.typelevel:cats-effect_2.13:3.3.0") + testImplementation("org.http4s:http4s-dsl_2.13:0.23.12") + +} + +jar { + manifest { + attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.http4s-ember-client-2.13_0.23' + } +} +verifyInstrumentation { + passes 'org.http4s:http4s-ember-client_2.13:[0.23.0,0.24.0)' + excludeRegex '.*(RC|M)[0-9]*' +} + +sourceSets.main.scala.srcDirs = ['src/main/scala', 'src/main/java'] +sourceSets.main.java.srcDirs = [] diff --git a/instrumentation-security/http4s-ember-client-2.13_0.23/src/main/java/org/http4s/EmberClientBuilder_Instrumentation.java b/instrumentation-security/http4s-ember-client-2.13_0.23/src/main/java/org/http4s/EmberClientBuilder_Instrumentation.java new file mode 100644 index 000000000..f702d27f7 --- /dev/null +++ b/instrumentation-security/http4s-ember-client-2.13_0.23/src/main/java/org/http4s/EmberClientBuilder_Instrumentation.java @@ -0,0 +1,19 @@ +package org.http4s; + +import cats.effect.kernel.Async; +import cats.effect.kernel.Resource; +import com.newrelic.agent.security.instrumentation.http4s.ember.NewrelicSecurityClientMiddleware$; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import org.http4s.client.Client; + +@Weave(type = MatchType.ExactClass, originalName = "org.http4s.ember.client.EmberClientBuilder") +public abstract class EmberClientBuilder_Instrumentation { + + private final Async evidence$1 = Weaver.callOriginal(); + public Resource> build() { + Resource> delegateResource = Weaver.callOriginal(); + return NewrelicSecurityClientMiddleware$.MODULE$.resource(delegateResource, evidence$1); + } +} diff --git a/instrumentation-security/http4s-ember-client-2.13_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/NewrelicSecurityClientMiddleware.scala b/instrumentation-security/http4s-ember-client-2.13_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/NewrelicSecurityClientMiddleware.scala new file mode 100644 index 000000000..4abb321f6 --- /dev/null +++ b/instrumentation-security/http4s-ember-client-2.13_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/NewrelicSecurityClientMiddleware.scala @@ -0,0 +1,126 @@ +package com.newrelic.agent.security.instrumentation.http4s.ember + +import cats.effect.kernel.Async +import cats.effect.{Resource, Sync} +import com.newrelic.api.agent.security.NewRelicSecurity +import com.newrelic.api.agent.security.instrumentation.helpers.{GenericHelper, ServletHelper} +import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityException +import com.newrelic.api.agent.security.schema.operation.SSRFOperation +import com.newrelic.api.agent.security.schema.{AbstractOperation, StringUtils, VulnerabilityCaseType} +import com.newrelic.api.agent.security.utils.SSRFUtils +import com.newrelic.api.agent.security.utils.logging.LogLevel +import org.http4s.Request +import org.http4s.client.Client + +import java.net.URI + +object NewrelicSecurityClientMiddleware { + + private final val nrSecCustomAttrName: String = "HTTP4S-EMBER-CLIENT-OUTBOUND" + private final val HTTP4S_EMBER_CLIENT: String = "HTTP4S-EMBER-CLIENT-2.13_0.23" + + private def construct[F[_] : Sync, T](t: T): F[T] = Sync[F].delay(t) + + private def clientResource[F[_] : Async](client: Client[F]): Client[F] = + Client { req: Request[F] => + for { + // pre-process hook + operation <- Resource.eval( + construct { + val isLockAcquired = GenericHelper.acquireLockIfPossible(VulnerabilityCaseType.HTTP_REQUEST, nrSecCustomAttrName) + var operation: AbstractOperation = null + if (isLockAcquired) { + operation = preprocessSecurityHook(req) + } + operation + }) + + // add Security Headers + request <- Resource.eval(construct {addSecurityHeaders(req, operation)}) + + // original call + response <- client.run(request) + + // post process and register exit event + newRes <- Resource.eval(construct{ + val isLockAcquired = GenericHelper.isLockAcquired(nrSecCustomAttrName); + if (isLockAcquired) { + GenericHelper.releaseLock(nrSecCustomAttrName) + } + registerExitOperation(isLockAcquired, operation) + response + }) + + } yield newRes + } + + private def addSecurityHeaders[F[_] : Async](request: Request[F], operation: AbstractOperation): Request[F] = { + val outboundRequest = new OutboundRequest(request) + if (operation != null) { + val securityMetaData = NewRelicSecurity.getAgent.getSecurityMetaData + val iastHeader = NewRelicSecurity.getAgent.getSecurityMetaData.getFuzzRequestIdentifier.getRaw + if (iastHeader != null && iastHeader.trim.nonEmpty) { + outboundRequest.setHeader(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID, iastHeader) + } + val csecParentId = securityMetaData.getCustomAttribute(GenericHelper.CSEC_PARENT_ID, classOf[String]) + if (StringUtils.isNotBlank(csecParentId)) { + outboundRequest.setHeader(GenericHelper.CSEC_PARENT_ID, csecParentId) + } + try { + NewRelicSecurity.getAgent.getSecurityMetaData.getMetaData.setFromJumpRequiredInStackTrace(Integer.valueOf(4)) + NewRelicSecurity.getAgent.registerOperation(operation) + } + finally { + if (operation.getApiID != null && operation.getApiID.trim.nonEmpty && operation.getExecutionId != null && operation.getExecutionId.trim.nonEmpty) { + outboundRequest.setHeader(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER, SSRFUtils.generateTracingHeaderValue(securityMetaData.getTracingHeaderValue, operation.getApiID, operation.getExecutionId, NewRelicSecurity.getAgent.getAgentUUID)) + } + } + } + outboundRequest.getRequest + } + + def resource[F[_] : Async](delegate: Resource[F, Client[F]]): Resource[F, Client[F]] = { + val res: Resource[F, Client[F]] = delegate.map(c =>clientResource(c)) + res + } + + private def preprocessSecurityHook[F[_] : Async](httpRequest: Request[F]): AbstractOperation = { + try { + val securityMetaData = NewRelicSecurity.getAgent.getSecurityMetaData + if (!NewRelicSecurity.isHookProcessingActive || securityMetaData.getRequest.isEmpty) return null + // Generate required URL + var methodURI: URI = null + var uri: String = null + try { + methodURI = new URI(httpRequest.uri.toString) + uri = methodURI.toString + if (methodURI == null) return null + } catch { + case ignored: Exception => + NewRelicSecurity.getAgent.log(LogLevel.WARNING, String.format(GenericHelper.URI_EXCEPTION_MESSAGE, HTTP4S_EMBER_CLIENT, ignored.getMessage), ignored, this.getClass.getName) + return null + } + return new SSRFOperation(uri, this.getClass.getName, "run") + } catch { + case e: Throwable => + if (e.isInstanceOf[NewRelicSecurityException]) { + NewRelicSecurity.getAgent.log(LogLevel.WARNING, String.format(GenericHelper.SECURITY_EXCEPTION_MESSAGE, HTTP4S_EMBER_CLIENT, e.getMessage), e, this.getClass.getName) + throw e + } + NewRelicSecurity.getAgent.log(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP4S_EMBER_CLIENT, e.getMessage), e, this.getClass.getName) + NewRelicSecurity.getAgent.reportIncident(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP4S_EMBER_CLIENT, e.getMessage), e, this.getClass.getName) + } + null + } + + private def registerExitOperation(isProcessingAllowed: Boolean, operation: AbstractOperation): Unit = { + try { + if (operation == null || !isProcessingAllowed || !NewRelicSecurity.isHookProcessingActive || NewRelicSecurity.getAgent.getSecurityMetaData.getRequest.isEmpty) return + NewRelicSecurity.getAgent.registerExitEvent(operation) + } catch { + case e: Throwable => + NewRelicSecurity.getAgent.log(LogLevel.FINEST, String.format(GenericHelper.EXIT_OPERATION_EXCEPTION_MESSAGE, HTTP4S_EMBER_CLIENT, e.getMessage), e, this.getClass.getName) + } + } +} + diff --git a/instrumentation-security/http4s-ember-client-2.13_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/OutboundRequest.scala b/instrumentation-security/http4s-ember-client-2.13_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/OutboundRequest.scala new file mode 100644 index 000000000..c19433953 --- /dev/null +++ b/instrumentation-security/http4s-ember-client-2.13_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/OutboundRequest.scala @@ -0,0 +1,20 @@ +package com.newrelic.agent.security.instrumentation.http4s.ember + +import org.http4s.{Header, Request} +import org.typelevel.ci.CIString + +/** + * Http4s's HttpRequest is immutable so we have to create a copy with the new headers. + */ + +class OutboundRequest[F[_]](request: Request[F]) { + private var req: Request[F] = request + + def setHeader(key: String, value: String): Unit = { + req = req.withHeaders(req.headers.put(Header.Raw.apply(CIString.apply(key), value))) + } + + def getRequest: Request[F] = { + req + } +} \ No newline at end of file diff --git a/instrumentation-security/http4s-ember-client-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/EmberClientTest.scala b/instrumentation-security/http4s-ember-client-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/EmberClientTest.scala new file mode 100644 index 000000000..ccb26c514 --- /dev/null +++ b/instrumentation-security/http4s-ember-client-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/EmberClientTest.scala @@ -0,0 +1,87 @@ +package com.nr.agent.security.instrumentation.blaze.client + +import cats.effect.unsafe.implicits.global +import cats.effect.{Async, IO} +import com.newrelic.agent.security.introspec.internal.HttpServerRule +import com.newrelic.agent.security.introspec.{InstrumentationTestConfig, SecurityInstrumentationTestRunner, SecurityIntrospector} +import com.newrelic.api.agent.security.instrumentation.helpers.{GenericHelper, ServletHelper} +import com.newrelic.api.agent.security.schema.operation.SSRFOperation +import com.newrelic.api.agent.security.schema.{AbstractOperation, VulnerabilityCaseType} +import com.nr.agent.security.instrumentation.blaze.client.Http4sTestUtils.makeRequest +import org.http4s.ember.client.EmberClientBuilder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.{Assert, FixMethodOrder, Rule, Test} + +import java.util +import java.util.UUID +import scala.concurrent.duration.DurationInt + +@RunWith(classOf[SecurityInstrumentationTestRunner]) +@InstrumentationTestConfig(includePrefixes = Array("org.http4s", "com.newrelic.agent.security.instrumentation.http4s")) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class EmberClientTest { + + @Rule + def server: HttpServerRule = httpServer + + val httpServer = new HttpServerRule() + + @Test + def blazeClientTest(): Unit = { + + val introspector: SecurityIntrospector = SecurityInstrumentationTestRunner.getIntrospector + makeRequest[IO](s"${server.getEndPoint}").unsafeRunTimed(2.seconds) + assertSSRFOperation(introspector.getOperations) + } + + @Test + def blazeClientTestWithHeaders(): Unit = { + val headerValue = String.valueOf(UUID.randomUUID) + + val introspector: SecurityIntrospector = SecurityInstrumentationTestRunner.getIntrospector + setCSECHeaders(headerValue = headerValue, introspector = introspector) + makeRequest[IO](s"${server.getEndPoint}").unsafeRunTimed(2.seconds) + assertSSRFOperation(introspector.getOperations) + verifyHeaders(headerValue, httpServer.getHeaders) + } + + + private def assertSSRFOperation(operations: util.List[AbstractOperation]): Unit = { + Assert.assertTrue("Incorrect number of operations detected!", operations.size == 1) + Assert.assertTrue("SSRFOperation not found!", operations.get(0).isInstanceOf[SSRFOperation]) + val operation: SSRFOperation = operations.get(0).asInstanceOf[SSRFOperation] + + Assert.assertFalse("operation should not be empty", operation.isEmpty) + Assert.assertFalse("JNDILookup should be false", operation.isJNDILookup) + Assert.assertFalse("LowSeverityHook should be disabled", operation.isLowSeverityHook) + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType) + Assert.assertEquals("Invalid executed method name.", "run", operation.getMethodName) + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint.toString, operation.getArg) + } + + private def verifyHeaders(headerValue: String, headers: util.Map[String, String]): Unit = { + 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 + "a", headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)) + Assert.assertTrue(String.format("Missing CSEC header: %s", GenericHelper.CSEC_PARENT_ID), headers.containsKey(GenericHelper.CSEC_PARENT_ID)) + Assert.assertEquals(String.format("Invalid CSEC header value for: %s", GenericHelper.CSEC_PARENT_ID), headerValue + "b", headers.get(GenericHelper.CSEC_PARENT_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)) + } + + private def setCSECHeaders(headerValue: String, introspector: SecurityIntrospector): Unit = { + introspector.setK2FuzzRequestId(headerValue + "a") + introspector.setK2ParentId(headerValue + "b") + introspector.setK2TracingData(headerValue) + } +} + +object Http4sTestUtils { + def makeRequest[F[_]: Async](url: String): F[String] = { + val client = EmberClientBuilder.default[F].build + client.use { c => + c.expect[String](url) + } + } +} + diff --git a/instrumentation-security/http4s-ember-server-2.12_0.23/build.gradle b/instrumentation-security/http4s-ember-server-2.12_0.23/build.gradle new file mode 100644 index 000000000..e6281a986 --- /dev/null +++ b/instrumentation-security/http4s-ember-server-2.12_0.23/build.gradle @@ -0,0 +1,27 @@ +apply plugin: 'scala' + +isScalaProjectEnabled(project, "scala-2.12") + +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.scala-lang:scala-library:2.12.14") + implementation('org.http4s:http4s-ember-server_2.12:0.23.12') + implementation("org.typelevel:cats-effect_2.12:3.3.0") + testImplementation("org.http4s:http4s-dsl_2.12:0.23.12") +} + +jar { + manifest { + attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.http4s-ember-server-2.12_0.23', 'Priority': '-1' + } +} + +verifyInstrumentation { + passes 'org.http4s:http4s-ember-server_2.12:[0.23.0,0.24.0)' + excludeRegex '.*(RC|M)[0-9]*' +} + +sourceSets.main.scala.srcDirs = ['src/main/scala', 'src/main/java'] +sourceSets.main.java.srcDirs = [] diff --git a/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/java/org/http4s/ember/server/EmberServerBuilder_Instrumentation.java b/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/java/org/http4s/ember/server/EmberServerBuilder_Instrumentation.java new file mode 100644 index 000000000..1a7b7e891 --- /dev/null +++ b/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/java/org/http4s/ember/server/EmberServerBuilder_Instrumentation.java @@ -0,0 +1,20 @@ +package org.http4s.ember.server; + +import cats.data.Kleisli; +import cats.effect.kernel.Async; +import com.newrelic.agent.security.http4s.ember.server.RequestProcessor$; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import org.http4s.Request; +import org.http4s.Response; + +@Weave(originalName = "org.http4s.ember.server.EmberServerBuilder") +public class EmberServerBuilder_Instrumentation { + + public final Async org$http4s$ember$server$EmberServerBuilder$$evidence$1 = Weaver.callOriginal(); + + public EmberServerBuilder_Instrumentation withHttpApp(Kleisli, Response> httpApp) { + httpApp = RequestProcessor$.MODULE$.genHttpApp(httpApp, this.org$http4s$ember$server$EmberServerBuilder$$evidence$1); + return Weaver.callOriginal(); + } +} diff --git a/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala b/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala new file mode 100644 index 000000000..c648c61e4 --- /dev/null +++ b/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala @@ -0,0 +1,186 @@ +package com.newrelic.agent.security.http4s.ember.server + +import cats.data.Kleisli +import cats.effect.Sync +import cats.implicits._ +import com.comcast.ip4s.Port +import com.newrelic.api.agent.security.NewRelicSecurity +import com.newrelic.api.agent.security.instrumentation.helpers.{GenericHelper, ICsecApiConstants, ServletHelper} +import com.newrelic.api.agent.security.schema._ +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 com.newrelic.api.agent.security.utils.logging.LogLevel +import fs2.RaiseThrowable +import org.http4s.{Headers, Message, Request, Response} + +import java.util + + +object RequestProcessor { + + private val METHOD_WITH_HTTP_APP = "withHttpApp" + private val HTTP_4S_EMBER_SERVER_2_12_0_23 = "HTTP4S-EMBER-SERVER-2.12_0.23" + private val X_FORWARDED_FOR = "x-forwarded-for" + + def genHttpApp[F[_] : Sync](httpApp: Kleisli[F, Request[F], Response[F]]): Kleisli[F, Request[F], Response[F]] = { + Kleisli { req: Request[F] => nrRequestResponse(req, httpApp) } + } + + private def nrRequestResponse[F[_] : Sync](request: Request[F], httpApp: Kleisli[F, Request[F], Response[F]]): F[Response[F]] = { + val result = construct((): Unit) + .redeemWith(_ => httpApp(request), + _ => for { + requestBody <- extractBody(request) + isLockAcquired <- preprocessHttpRequest(request, requestBody) + resp <- httpApp(request) + responseBody <- extractBody(resp) + _ <- postProcessSecurityHook(isLockAcquired, resp, responseBody) + } yield resp + ) + result + } + + private def preprocessHttpRequest[F[_]: Sync](request: Request[F], body: String): F[Boolean] = construct { + val isLockAcquired = GenericHelper.acquireLockIfPossible("HTTP4S-EMBER-REQUEST_LOCK", request.hashCode()) + try { + if (isLockAcquired && !NewRelicSecurity.getAgent.getSecurityMetaData.getRequest.isRequestParsed){ + + val securityMetaData: SecurityMetaData = NewRelicSecurity.getAgent.getSecurityMetaData + val securityRequest: HttpRequest = securityMetaData.getRequest + val securityAgentMetaData: AgentMetaData = securityMetaData.getMetaData + + securityRequest.setMethod(request.method.name) + securityRequest.setServerPort((request.serverPort).get.asInstanceOf[Port].value) + securityRequest.setClientIP(request.remoteAddr.get.toString) + if(request.isSecure.get){ + securityRequest.setProtocol("https") + } else { + securityRequest.setProtocol("http") + } + + securityRequest.setUrl(request.uri.toString) + + if (securityRequest.getClientIP != null && securityRequest.getClientIP.trim.nonEmpty) { + securityAgentMetaData.getIps.add(securityRequest.getClientIP) + securityRequest.setClientPort(String.valueOf(request.remotePort.get)) + } + + processRequestHeaders(request.headers, securityRequest) + securityMetaData.setTracingHeaderValue(getTraceHeader(securityRequest.getHeaders)) + securityRequest.setContentType(getContentType(securityRequest.getHeaders)) + securityRequest.getBody.append(body) + + val trace: Array[StackTraceElement] = Thread.currentThread.getStackTrace + securityMetaData.getMetaData.setServiceTrace(util.Arrays.copyOfRange(trace, 1, trace.length)) + securityRequest.setRequestParsed(true) + } + + } catch { + case e: Throwable => NewRelicSecurity.getAgent.log(LogLevel.WARNING, String.format(GenericHelper.ERROR_GENERATING_HTTP_REQUEST, HTTP_4S_EMBER_SERVER_2_12_0_23, e.getMessage), e, this.getClass.getName) + } + isLockAcquired + } + + private def postProcessSecurityHook[F[_]: Sync](isLockAcquired: Boolean, response: Response[F], body: String): F[Unit] = construct { + try { + if (isLockAcquired && NewRelicSecurity.isHookProcessingActive && !NewRelicSecurity.getAgent.getIastDetectionCategory.getRxssEnabled) { + val securityResponse = NewRelicSecurity.getAgent.getSecurityMetaData.getResponse + securityResponse.setResponseCode(response.status.code) + processResponseHeaders(response.headers, securityResponse) + securityResponse.setResponseContentType(getContentType(securityResponse.getHeaders)) + + securityResponse.getResponseBody.append(body) + + ServletHelper.executeBeforeExitingTransaction() + if (!ServletHelper.isResponseContentTypeExcluded(NewRelicSecurity.getAgent.getSecurityMetaData.getResponse.getResponseContentType)) { + val rxssOperation = new RXSSOperation(NewRelicSecurity.getAgent.getSecurityMetaData.getRequest, NewRelicSecurity.getAgent.getSecurityMetaData.getResponse, this.getClass.getName, METHOD_WITH_HTTP_APP) + NewRelicSecurity.getAgent.registerOperation(rxssOperation) + } + } + } catch { + case e: Throwable => + if (e.isInstanceOf[NewRelicSecurityException]) { + NewRelicSecurity.getAgent.log(LogLevel.WARNING, String.format(GenericHelper.SECURITY_EXCEPTION_MESSAGE, HTTP_4S_EMBER_SERVER_2_12_0_23, e.getMessage), e, this.getClass.getName) + throw e + } + NewRelicSecurity.getAgent.log(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP_4S_EMBER_SERVER_2_12_0_23, e.getMessage), e, this.getClass.getName) + NewRelicSecurity.getAgent.reportIncident(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP_4S_EMBER_SERVER_2_12_0_23, e.getMessage), e, this.getClass.getName) + } + } + + private def extractBody[F[_]: Sync](msg: Message[F]): F[String] = { + if (msg.contentType.nonEmpty && msg.contentType.get.charset.nonEmpty) { + val charset = msg.contentType.get.charset.get; + msg.bodyText(RaiseThrowable.fromApplicativeError, defaultCharset = charset).compile.string + } else { + msg.bodyText.compile.string + } + } + + private def processRequestHeaders(headers: Headers, securityRequest: HttpRequest): Unit = { + headers.foreach(header => { + var takeNextValue = false + var headerKey: String = StringUtils.EMPTY + if (header.name != null && header.name.nonEmpty) { + headerKey = header.name.toString + } + val headerValue: String = header.value + + val agentPolicy: AgentPolicy = NewRelicSecurity.getAgent.getCurrentPolicy + val 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 == headerKey) { + // TODO: May think of removing this intermediate obj and directly create K2 Identifier. + NewRelicSecurity.getAgent.getSecurityMetaData.setFuzzRequestIdentifier(ServletHelper.parseFuzzRequestIdentifierHeader(headerValue)) + } + if (GenericHelper.CSEC_PARENT_ID == headerKey) { + NewRelicSecurity.getAgent.getSecurityMetaData.addCustomAttribute(GenericHelper.CSEC_PARENT_ID, headerValue) + } + else if (ICsecApiConstants.NR_CSEC_JAVA_HEAD_REQUEST == headerKey) { + NewRelicSecurity.getAgent.getSecurityMetaData.addCustomAttribute(ICsecApiConstants.NR_CSEC_JAVA_HEAD_REQUEST, true) + } + + if (headerValue != null && headerValue.trim.nonEmpty) { + if (takeNextValue) { + agentMetaData.setClientDetectedFromXFF(true) + securityRequest.setClientIP(headerValue) + agentMetaData.getIps.add(securityRequest.getClientIP) + securityRequest.setClientPort(StringUtils.EMPTY) + takeNextValue = false + } + } + securityRequest.getHeaders.put(headerKey.toLowerCase, headerValue) + }) + } + + private def processResponseHeaders(headers: Headers, securityResp: HttpResponse): Unit = { + headers.foreach(header => { + if (header.name != null && header.name.nonEmpty) { + securityResp.getHeaders.put(header.name.toString.toLowerCase, header.value) + } + }) + } + + private def getContentType(headers: util.Map[String, String]): String = { + var contentType = StringUtils.EMPTY + if (headers.containsKey("content-type")) contentType = headers.get("content-type") + contentType + } + + private def getTraceHeader(headers: util.Map[String, String]): String = { + var data = StringUtils.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) + } + data + } + + private def construct[F[_] : Sync, T](t: => T): F[T] = Sync[F].delay(t) +} diff --git a/instrumentation-security/http4s-ember-server-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/EmberServerBuilderTest.scala b/instrumentation-security/http4s-ember-server-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/EmberServerBuilderTest.scala new file mode 100644 index 000000000..1c289baf7 --- /dev/null +++ b/instrumentation-security/http4s-ember-server-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/EmberServerBuilderTest.scala @@ -0,0 +1,137 @@ +package com.nr.agent.security.instrumentation.ember.server + +import cats.effect.IO +import com.newrelic.agent.security.introspec.{InstrumentationTestConfig, SecurityInstrumentationTestRunner, SecurityIntrospector} +import com.newrelic.api.agent.security.instrumentation.helpers.{GenericHelper, ServletHelper} +import com.newrelic.api.agent.security.schema.operation.RXSSOperation +import com.newrelic.api.agent.security.schema.{SecurityMetaData, VulnerabilityCaseType} +import org.http4s.dsl.io._ +import org.http4s.implicits._ +import org.http4s.{Header, HttpRoutes} +import org.junit.runner.RunWith +import org.junit.{After, Assert, Before, Test} +import org.typelevel.ci.CIString + +import java.net.{HttpURLConnection, URL} +import java.util +import java.util.UUID + +@RunWith(classOf[SecurityInstrumentationTestRunner]) +@InstrumentationTestConfig(includePrefixes = Array("org.http4s.ember", "com.newrelic.agent.security.http4s.ember.server", "scala")) +class EmberServerBuilderTest { + + val hostname = "0.0.0.0" + val port: Int = SecurityInstrumentationTestRunner.getIntrospector.getRandomPort + val contentType: String = "text/plain" + + val emberServer = new Http4sTestServer(hostname, port, + HttpRoutes.of[IO] { + case _ -> Root / "hello" / name => + Ok(s"Hello, $name.").map(_.putHeaders(Header.Raw(CIString.apply("content-type"), contentType))) + }.orNotFound) + + @Before + def setup(): Unit = { + emberServer.start() + } + + @After + def reset(): Unit = { + emberServer.stop() + } + + + @Test + def emberServerTest(): Unit = { + val introspector: SecurityIntrospector = SecurityInstrumentationTestRunner.getIntrospector + Http4sTestUtils.makeRequest(s"http://$hostname:$port/hello/bob", addCSECHeader = false, "") + + val operations = introspector.getOperations + Assert.assertTrue(operations.size() > 0) + Assert.assertTrue(operations.get(0).isInstanceOf[RXSSOperation]) + + assertRXSSOperation(operations.get(0).asInstanceOf[RXSSOperation]) + assertMetaData(introspector.getSecurityMetaData) + } + + @Test + def emberServerHeaderTest(): Unit = { + val headerValue: String = String.valueOf(UUID.randomUUID()) + + val introspector: SecurityIntrospector = SecurityInstrumentationTestRunner.getIntrospector + Http4sTestUtils.makeRequest(s"http://$hostname:$port/hello/bob", addCSECHeader = true, headerValue) + + val operations = introspector.getOperations + Assert.assertTrue(operations.size() > 0) + Assert.assertTrue(operations.get(0).isInstanceOf[RXSSOperation]) + + assertRXSSOperation(operations.get(0).asInstanceOf[RXSSOperation]) + assertMetaData(introspector.getSecurityMetaData) + assertCSECHeaders(headerValue, introspector.getSecurityMetaData.getRequest.getHeaders) + } + + private def assertCSECHeaders(headerValue: String, headers: util.Map[String, String]): Unit = { + 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 + "a", headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)) + Assert.assertTrue(String.format("Missing CSEC header: %s", GenericHelper.CSEC_PARENT_ID), headers.containsKey(GenericHelper.CSEC_PARENT_ID)) + Assert.assertEquals(String.format("Invalid CSEC header value for: %s", GenericHelper.CSEC_PARENT_ID), headerValue + "b", headers.get(GenericHelper.CSEC_PARENT_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)) + } + + private def assertRXSSOperation(operation: RXSSOperation): Unit = { + Assert.assertFalse("LowSeverityHook should be disabled", operation.isLowSeverityHook) + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.REFLECTED_XSS, operation.getCaseType) + Assert.assertEquals("Invalid executed method name.", "withHttpApp", operation.getMethodName) + + Assert.assertFalse("request should not be empty", operation.getRequest.isEmpty) + Assert.assertEquals("Invalid Request content-type.", contentType, operation.getRequest.getContentType) + Assert.assertFalse("Headers should not be empty", operation.getRequest.getHeaders.isEmpty) + Assert.assertEquals("Invalid Request body", "body extract", operation.getRequest.getBody.toString) + + + Assert.assertFalse("response should not be empty", operation.getResponse.isEmpty) + Assert.assertEquals("Invalid response content-type.", contentType, operation.getResponse.getResponseContentType) + Assert.assertFalse("Headers should not be empty", operation.getResponse.getHeaders.isEmpty) + Assert.assertEquals("Invalid Response body", "Hello, bob.", operation.getResponse.getResponseBody.toString) + Assert.assertEquals("Invalid Response code", 200, operation.getResponse.getResponseCode) + } + + private def assertMetaData(metaData: SecurityMetaData): Unit = { + Assert.assertFalse("request should not be empty", metaData.getRequest.isEmpty) + Assert.assertEquals("Invalid Request content-type.", contentType, metaData.getRequest.getContentType) + Assert.assertEquals("Invalid protocol.", "http", metaData.getRequest.getProtocol) + + Assert.assertEquals("Invalid Request body", "body extract", metaData.getRequest.getBody.toString) + Assert.assertFalse("Headers should not be empty", metaData.getRequest.getHeaders.isEmpty) + + Assert.assertFalse("response should not be empty", metaData.getResponse.isEmpty) + Assert.assertEquals("Invalid response content-type.", contentType, metaData.getResponse.getResponseContentType) + Assert.assertEquals("Invalid Response code", 200, metaData.getResponse.getResponseCode) + Assert.assertFalse("Headers should not be empty", metaData.getResponse.getHeaders.isEmpty) + Assert.assertEquals("Invalid Response body", "Hello, bob.", metaData.getResponse.getResponseBody.toString) + } +} + +object Http4sTestUtils { + def makeRequest(url: String, addCSECHeader: Boolean, headerValue: String): Unit = { + val u: URL = new URL(url) + val conn = u.openConnection.asInstanceOf[HttpURLConnection] + + conn.setDoOutput(true) + conn.setRequestProperty("content-type", "text/plain") + + if (addCSECHeader) { + conn.setRequestProperty(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID, headerValue + "a") + conn.setRequestProperty(GenericHelper.CSEC_PARENT_ID, headerValue + "b") + conn.setRequestProperty(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER, String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", headerValue)) + } + + val stream = conn.getOutputStream + stream.write("body extract".getBytes) + + conn.connect() + println(conn.getResponseCode) + } +} + diff --git a/instrumentation-security/http4s-ember-server-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/Http4sTestServer.scala b/instrumentation-security/http4s-ember-server-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/Http4sTestServer.scala new file mode 100644 index 000000000..d1eed636d --- /dev/null +++ b/instrumentation-security/http4s-ember-server-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/Http4sTestServer.scala @@ -0,0 +1,30 @@ +package com.nr.agent.security.instrumentation.ember.server + +import cats.effect.{IO, Resource} +import com.comcast.ip4s._ +import org.http4s.HttpApp +import org.http4s.server.Server +import org.http4s.ember.server.EmberServerBuilder +import cats.effect.unsafe.implicits.global + +class Http4sTestServer(val testServerHost: String, val port: Int, val httpApp: HttpApp[IO]) { + + var server: Server = _ + var finalizer: IO[Unit] = _ + + val serverResource: Resource[IO, Server] = EmberServerBuilder.default[IO] + .withHttpApp(httpApp) + .withHost(Host.fromString(testServerHost).orNull) + .withPort(Port.fromInt(port).get) + .build + + def start(): Unit = { + val materializedServer = serverResource.allocated.unsafeRunSync() + server = materializedServer._1 + finalizer = materializedServer._2 + } + + def stop(): Unit = finalizer.unsafeRunSync() + + def hostname: String = server.address.getHostName +} diff --git a/instrumentation-security/http4s-ember-server-2.13_0.23/build.gradle b/instrumentation-security/http4s-ember-server-2.13_0.23/build.gradle new file mode 100644 index 000000000..6f9c24c92 --- /dev/null +++ b/instrumentation-security/http4s-ember-server-2.13_0.23/build.gradle @@ -0,0 +1,27 @@ +apply plugin: 'scala' + +isScalaProjectEnabled(project, "scala-2.13") + +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.scala-lang:scala-library:2.13.3") + implementation('org.http4s:http4s-ember-server_2.13:0.23.12') + implementation("org.typelevel:cats-effect_2.13:3.3.12"){transitive = false} + testImplementation("org.http4s:http4s-dsl_2.13:0.23.12") +} + +jar { + manifest { + attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.http4s-ember-server-2.13_0.23', 'Priority': '-1' + } +} + +verifyInstrumentation { + passes 'org.http4s:http4s-ember-server_2.13:[0.23.0,0.24.0)' + excludeRegex '.*(RC|M)[0-9]*' +} + +sourceSets.main.scala.srcDirs = ['src/main/scala', 'src/main/java'] +sourceSets.main.java.srcDirs = [] diff --git a/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/java/org/http4s/ember/server/EmberServerBuilder_Instrumentation.java b/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/java/org/http4s/ember/server/EmberServerBuilder_Instrumentation.java new file mode 100644 index 000000000..d7beb29c2 --- /dev/null +++ b/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/java/org/http4s/ember/server/EmberServerBuilder_Instrumentation.java @@ -0,0 +1,21 @@ +package org.http4s.ember.server; + +import cats.data.Kleisli; +import cats.effect.kernel.Async; +import com.newrelic.agent.security.http4s.ember.server.RequestProcessor$; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import org.http4s.Request; +import org.http4s.Response; + +@Weave(originalName = "org.http4s.ember.server.EmberServerBuilder") +public class EmberServerBuilder_Instrumentation { + + public final Async org$http4s$ember$server$EmberServerBuilder$$evidence$1 = Weaver.callOriginal(); + + public EmberServerBuilder withHttpApp(Kleisli, Response> httpApp) { + httpApp = RequestProcessor$.MODULE$.genHttpApp(httpApp, this.org$http4s$ember$server$EmberServerBuilder$$evidence$1); + return Weaver.callOriginal(); + } +} + diff --git a/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala b/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala new file mode 100644 index 000000000..536ecfc10 --- /dev/null +++ b/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala @@ -0,0 +1,186 @@ +package com.newrelic.agent.security.http4s.ember.server + +import cats.data.Kleisli +import cats.effect.Sync +import cats.implicits._ +import com.comcast.ip4s.Port +import com.newrelic.api.agent.security.NewRelicSecurity +import com.newrelic.api.agent.security.instrumentation.helpers.{GenericHelper, ICsecApiConstants, ServletHelper} +import com.newrelic.api.agent.security.schema._ +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 com.newrelic.api.agent.security.utils.logging.LogLevel +import fs2.RaiseThrowable +import org.http4s.{Headers, Message, Request, Response} + +import java.util + + +object RequestProcessor { + + private val METHOD_WITH_HTTP_APP = "withHttpApp" + private val HTTP_4S_EMBER_SERVER_2_13_0_23 = "HTTP4S-EMBER-SERVER-2.13_0.23" + private val X_FORWARDED_FOR = "x-forwarded-for" + + def genHttpApp[F[_] : Sync](httpApp: Kleisli[F, Request[F], Response[F]]): Kleisli[F, Request[F], Response[F]] = { + Kleisli { req: Request[F] => nrRequestResponse(req, httpApp) } + } + + private def nrRequestResponse[F[_] : Sync](request: Request[F], httpApp: Kleisli[F, Request[F], Response[F]]): F[Response[F]] = { + val result = construct((): Unit) + .redeemWith(_ => httpApp(request), + _ => for { + requestBody <- extractBody(request) + isLockAcquired <- preprocessHttpRequest(request, requestBody) + resp <- httpApp(request) + responseBody <- extractBody(resp) + _ <- postProcessSecurityHook(isLockAcquired, resp, responseBody) + } yield resp + ) + result + } + + private def preprocessHttpRequest[F[_]: Sync](request: Request[F], body: String): F[Boolean] = construct { + val isLockAcquired = GenericHelper.acquireLockIfPossible("HTTP4S-EMBER-REQUEST_LOCK", request.hashCode()) + try { + if (isLockAcquired && !NewRelicSecurity.getAgent.getSecurityMetaData.getRequest.isRequestParsed){ + + val securityMetaData: SecurityMetaData = NewRelicSecurity.getAgent.getSecurityMetaData + val securityRequest: HttpRequest = securityMetaData.getRequest + val securityAgentMetaData: AgentMetaData = securityMetaData.getMetaData + + securityRequest.setMethod(request.method.name) + securityRequest.setServerPort((request.serverPort).get.asInstanceOf[Port].value) + securityRequest.setClientIP(request.remoteAddr.get.toString) + if(request.isSecure.get){ + securityRequest.setProtocol("https") + } else { + securityRequest.setProtocol("http") + } + securityRequest.setUrl(request.uri.toString) + + if (securityRequest.getClientIP != null && securityRequest.getClientIP.trim.nonEmpty) { + securityAgentMetaData.getIps.add(securityRequest.getClientIP) + securityRequest.setClientPort(String.valueOf(request.remotePort.get)) + } + + processRequestHeaders(request.headers, securityRequest) + securityMetaData.setTracingHeaderValue(getTraceHeader(securityRequest.getHeaders)) + securityRequest.setContentType(getContentType(securityRequest.getHeaders)) + + securityRequest.getBody.append(body) + + val trace: Array[StackTraceElement] = Thread.currentThread.getStackTrace + securityMetaData.getMetaData.setServiceTrace(util.Arrays.copyOfRange(trace, 1, trace.length)) + securityRequest.setRequestParsed(true) + } + + } catch { + case e: Throwable => NewRelicSecurity.getAgent.log(LogLevel.WARNING, String.format(GenericHelper.ERROR_GENERATING_HTTP_REQUEST, HTTP_4S_EMBER_SERVER_2_13_0_23, e.getMessage), e, this.getClass.getName) + } + isLockAcquired + } + + private def extractBody[F[_]: Sync](msg: Message[F]): F[String] = { + if (msg.contentType.nonEmpty && msg.contentType.get.charset.nonEmpty) { + val charset = msg.contentType.get.charset.get; + msg.bodyText(RaiseThrowable.fromApplicativeError, defaultCharset = charset).compile.string + } else { + msg.bodyText.compile.string + } + } + + private def postProcessSecurityHook[F[_]: Sync](isLockAcquired: Boolean, response: Response[F], body: String): F[Unit] = construct { + try { + if (isLockAcquired && NewRelicSecurity.isHookProcessingActive && !NewRelicSecurity.getAgent.getIastDetectionCategory.getRxssEnabled) { + val securityResponse = NewRelicSecurity.getAgent.getSecurityMetaData.getResponse + securityResponse.setResponseCode(response.status.code) + processResponseHeaders(response.headers, securityResponse) + securityResponse.setResponseContentType(getContentType(securityResponse.getHeaders)) + + securityResponse.getResponseBody.append(body) + + ServletHelper.executeBeforeExitingTransaction() + if (!ServletHelper.isResponseContentTypeExcluded(NewRelicSecurity.getAgent.getSecurityMetaData.getResponse.getResponseContentType)) { + val rxssOperation = new RXSSOperation(NewRelicSecurity.getAgent.getSecurityMetaData.getRequest, NewRelicSecurity.getAgent.getSecurityMetaData.getResponse, this.getClass.getName, METHOD_WITH_HTTP_APP) + NewRelicSecurity.getAgent.registerOperation(rxssOperation) + } + } + } catch { + case e: Throwable => + if (e.isInstanceOf[NewRelicSecurityException]) { + NewRelicSecurity.getAgent.log(LogLevel.WARNING, String.format(GenericHelper.SECURITY_EXCEPTION_MESSAGE, HTTP_4S_EMBER_SERVER_2_13_0_23, e.getMessage), e, this.getClass.getName) + throw e + } + NewRelicSecurity.getAgent.log(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP_4S_EMBER_SERVER_2_13_0_23, e.getMessage), e, this.getClass.getName) + NewRelicSecurity.getAgent.reportIncident(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP_4S_EMBER_SERVER_2_13_0_23, e.getMessage), e, this.getClass.getName) + } + } + + private def processRequestHeaders(headers: Headers, securityRequest: HttpRequest): Unit = { + headers.foreach(header => { + var takeNextValue = false + var headerKey: String = StringUtils.EMPTY + if (header.name != null && header.name.nonEmpty) { + headerKey = header.name.toString + } + val headerValue: String = header.value + + val agentPolicy: AgentPolicy = NewRelicSecurity.getAgent.getCurrentPolicy + val 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 == headerKey) { + // TODO: May think of removing this intermediate obj and directly create K2 Identifier. + NewRelicSecurity.getAgent.getSecurityMetaData.setFuzzRequestIdentifier(ServletHelper.parseFuzzRequestIdentifierHeader(headerValue)) + } + if (GenericHelper.CSEC_PARENT_ID == headerKey) { + NewRelicSecurity.getAgent.getSecurityMetaData.addCustomAttribute(GenericHelper.CSEC_PARENT_ID, headerValue) + } + else if (ICsecApiConstants.NR_CSEC_JAVA_HEAD_REQUEST == headerKey) { + NewRelicSecurity.getAgent.getSecurityMetaData.addCustomAttribute(ICsecApiConstants.NR_CSEC_JAVA_HEAD_REQUEST, true) + } + + if (headerValue != null && headerValue.trim.nonEmpty) { + if (takeNextValue) { + agentMetaData.setClientDetectedFromXFF(true) + securityRequest.setClientIP(headerValue) + agentMetaData.getIps.add(securityRequest.getClientIP) + securityRequest.setClientPort(StringUtils.EMPTY) + takeNextValue = false + } + } + securityRequest.getHeaders.put(headerKey.toLowerCase, headerValue) + }) + } + + private def processResponseHeaders(headers: Headers, securityResp: HttpResponse): Unit = { + headers.foreach(header => { + if (header.name != null && header.name.nonEmpty) { + securityResp.getHeaders.put(header.name.toString.toLowerCase, header.value) + } + }) + } + + private def getContentType(headers: util.Map[String, String]): String = { + var contentType = StringUtils.EMPTY + if (headers.containsKey("content-type")) contentType = headers.get("content-type") + contentType + } + + private def getTraceHeader(headers: util.Map[String, String]): String = { + var data = StringUtils.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) + } + data + } + + private def construct[F[_] : Sync, T](t: => T): F[T] = Sync[F].delay(t) +} diff --git a/instrumentation-security/http4s-ember-server-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/EmberServerBuilderTest.scala b/instrumentation-security/http4s-ember-server-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/EmberServerBuilderTest.scala new file mode 100644 index 000000000..44e5ef126 --- /dev/null +++ b/instrumentation-security/http4s-ember-server-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/EmberServerBuilderTest.scala @@ -0,0 +1,136 @@ +package com.nr.agent.security.instrumentation.ember.server + +import cats.effect.IO +import com.newrelic.agent.security.introspec.{InstrumentationTestConfig, SecurityInstrumentationTestRunner, SecurityIntrospector} +import com.newrelic.api.agent.security.instrumentation.helpers.{GenericHelper, ServletHelper} +import com.newrelic.api.agent.security.schema.operation.RXSSOperation +import com.newrelic.api.agent.security.schema.{SecurityMetaData, VulnerabilityCaseType} +import org.http4s.dsl.io._ +import org.http4s.implicits._ +import org.http4s.{Header, HttpRoutes} +import org.junit.runner.RunWith +import org.junit.{After, Assert, Before, Test} +import org.typelevel.ci.CIString + +import java.net.{HttpURLConnection, URL} +import java.util +import java.util.UUID + +@RunWith(classOf[SecurityInstrumentationTestRunner]) +@InstrumentationTestConfig(includePrefixes = Array("org.http4s", "com.newrelic.agent.security.http4s.ember.server", "scala")) +class EmberServerBuilderTest { + + val hostname = "0.0.0.0" + val port: Int = SecurityInstrumentationTestRunner.getIntrospector.getRandomPort + val contentType: String = "text/plain" + + val emberServer = new Http4sTestServer(hostname, port, + HttpRoutes.of[IO] { + case _ -> Root / "hello" / name => + Ok(s"Hello, $name.").map(_.putHeaders(Header.Raw(CIString.apply("content-type"), contentType))) + }.orNotFound) + + @Before + def setup(): Unit = { + emberServer.start() + } + + @After + def reset(): Unit = { + emberServer.stop() + } + + + @Test + def emberServerTest(): Unit = { + val introspector: SecurityIntrospector = SecurityInstrumentationTestRunner.getIntrospector + Http4sTestUtils.makeRequest(s"http://$hostname:$port/hello/bob", addCSECHeader = false, "") + + val operations = introspector.getOperations + Assert.assertTrue(operations.size() > 0) + Assert.assertTrue(operations.get(0).isInstanceOf[RXSSOperation]) + + assertRXSSOperation(operations.get(0).asInstanceOf[RXSSOperation]) + assertMetaData(introspector.getSecurityMetaData) + } + + @Test + def emberServerHeaderTest(): Unit = { + val headerValue: String = String.valueOf(UUID.randomUUID()) + + val introspector: SecurityIntrospector = SecurityInstrumentationTestRunner.getIntrospector + Http4sTestUtils.makeRequest(s"http://$hostname:$port/hello/bob", addCSECHeader = true, headerValue) + + val operations = introspector.getOperations + Assert.assertTrue(operations.size() > 0) + Assert.assertTrue(operations.get(0).isInstanceOf[RXSSOperation]) + + assertRXSSOperation(operations.get(0).asInstanceOf[RXSSOperation]) + assertMetaData(introspector.getSecurityMetaData) + assertCSECHeaders(headerValue, introspector.getSecurityMetaData.getRequest.getHeaders) + } + + private def assertCSECHeaders(headerValue: String, headers: util.Map[String, String]): Unit = { + 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 + "a", headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)) + Assert.assertTrue(String.format("Missing CSEC header: %s", GenericHelper.CSEC_PARENT_ID), headers.containsKey(GenericHelper.CSEC_PARENT_ID)) + Assert.assertEquals(String.format("Invalid CSEC header value for: %s", GenericHelper.CSEC_PARENT_ID), headerValue + "b", headers.get(GenericHelper.CSEC_PARENT_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)) + } + + private def assertRXSSOperation(operation: RXSSOperation): Unit = { + Assert.assertFalse("LowSeverityHook should be disabled", operation.isLowSeverityHook) + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.REFLECTED_XSS, operation.getCaseType) + Assert.assertEquals("Invalid executed method name.", "withHttpApp", operation.getMethodName) + + Assert.assertFalse("request should not be empty", operation.getRequest.isEmpty) + Assert.assertEquals("Invalid Request content-type.", contentType, operation.getRequest.getContentType) + Assert.assertFalse("Headers should not be empty", operation.getRequest.getHeaders.isEmpty) + Assert.assertEquals("Invalid Request body", "body extract", operation.getRequest.getBody.toString) + + + Assert.assertFalse("response should not be empty", operation.getResponse.isEmpty) + Assert.assertEquals("Invalid response content-type.", contentType, operation.getResponse.getResponseContentType) + Assert.assertFalse("Headers should not be empty", operation.getResponse.getHeaders.isEmpty) + Assert.assertEquals("Invalid Response body", "Hello, bob.", operation.getResponse.getResponseBody.toString) + Assert.assertEquals("Invalid Response code", 200, operation.getResponse.getResponseCode) + } + + private def assertMetaData(metaData: SecurityMetaData): Unit = { + Assert.assertFalse("request should not be empty", metaData.getRequest.isEmpty) + Assert.assertEquals("Invalid Request content-type.", contentType, metaData.getRequest.getContentType) + Assert.assertEquals("Invalid protocol.", "http", metaData.getRequest.getProtocol) + + Assert.assertEquals("Invalid Request body", "body extract", metaData.getRequest.getBody.toString) + Assert.assertFalse("Headers should not be empty", metaData.getRequest.getHeaders.isEmpty) + + Assert.assertFalse("response should not be empty", metaData.getResponse.isEmpty) + Assert.assertEquals("Invalid response content-type.", contentType, metaData.getResponse.getResponseContentType) + Assert.assertEquals("Invalid Response code", 200, metaData.getResponse.getResponseCode) + Assert.assertFalse("Headers should not be empty", metaData.getResponse.getHeaders.isEmpty) + Assert.assertEquals("Invalid Response body", "Hello, bob.", metaData.getResponse.getResponseBody.toString) + } +} + +object Http4sTestUtils { + def makeRequest(url: String, addCSECHeader: Boolean, headerValue: String): Unit = { + val u: URL = new URL(url) + val conn = u.openConnection.asInstanceOf[HttpURLConnection] + + conn.setDoOutput(true) + conn.setRequestProperty("content-type", "text/plain") + + if (addCSECHeader) { + conn.setRequestProperty(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID, headerValue + "a") + conn.setRequestProperty(GenericHelper.CSEC_PARENT_ID, headerValue + "b") + conn.setRequestProperty(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER, String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", headerValue)) + } + + val stream = conn.getOutputStream + stream.write("body extract".getBytes) + + conn.connect() + println(conn.getResponseCode) + } +} diff --git a/instrumentation-security/http4s-ember-server-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/Http4sTestServer.scala b/instrumentation-security/http4s-ember-server-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/Http4sTestServer.scala new file mode 100644 index 000000000..d1eed636d --- /dev/null +++ b/instrumentation-security/http4s-ember-server-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/Http4sTestServer.scala @@ -0,0 +1,30 @@ +package com.nr.agent.security.instrumentation.ember.server + +import cats.effect.{IO, Resource} +import com.comcast.ip4s._ +import org.http4s.HttpApp +import org.http4s.server.Server +import org.http4s.ember.server.EmberServerBuilder +import cats.effect.unsafe.implicits.global + +class Http4sTestServer(val testServerHost: String, val port: Int, val httpApp: HttpApp[IO]) { + + var server: Server = _ + var finalizer: IO[Unit] = _ + + val serverResource: Resource[IO, Server] = EmberServerBuilder.default[IO] + .withHttpApp(httpApp) + .withHost(Host.fromString(testServerHost).orNull) + .withPort(Port.fromInt(port).get) + .build + + def start(): Unit = { + val materializedServer = serverResource.allocated.unsafeRunSync() + server = materializedServer._1 + finalizer = materializedServer._2 + } + + def stop(): Unit = finalizer.unsafeRunSync() + + def hostname: String = server.address.getHostName +} diff --git a/settings.gradle b/settings.gradle index ef08b459c..ab12bfd9e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -218,4 +218,8 @@ include 'instrumentation:solr-8.0.0' include 'instrumentation:solr-9.0.0' include 'instrumentation:graphql-java-16.2' include 'instrumentation:websphere-liberty-profile-environment-8.5.5.5' -include 'instrumentation:apache-pekko-http-core-2.13_1' +include 'instrumentation:http4s-ember-server-2.12_0.23' +include 'instrumentation:http4s-ember-server-2.13_0.23' +include 'instrumentation:http4s-ember-client-2.13_0.23' +include 'instrumentation:http4s-ember-client-2.12_0.23' +include 'instrumentation:apache-pekko-http-core-2.13_1' \ No newline at end of file