diff --git a/Changelog.md b/Changelog.md index a680acb6d..4b1b049d6 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,6 +4,25 @@ 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.6.0] - 2024-12-16 +### Adds +- [PR-329](https://github.com/newrelic/csec-java-agent/pull/329) Apache Pekko Server Support: The security agent now supports Apache Pekko Server version 1.0.0 and newer, compatible with Scala 2.13 and above. [NR-308780](https://new-relic.atlassian.net/browse/NR-308780), [NR-308781](https://new-relic.atlassian.net/browse/NR-308781), [NR-308791](https://new-relic.atlassian.net/browse/NR-308791), [NR-308792](https://new-relic.atlassian.net/browse/NR-308792) [NR-308782](https://new-relic.atlassian.net/browse/NR-308782) +- [PR-228](https://github.com/newrelic/csec-java-agent/pull/228) HTTP4s Ember Server Support: Added support for HTTP4s Ember Server version 0.23 and newer, compatible with Scala 2.12 and above. [NR-293957](https://new-relic.atlassian.net/browse/NR-293957), [NR-293847](https://new-relic.atlassian.net/browse/NR-293847), [NR-293844](https://new-relic.atlassian.net/browse/NR-293844) +- [PR-344](https://github.com/newrelic/csec-java-agent/pull/344) HTTP4s Blaze Server Support: The security agent now supports HTTP4s Blaze Server version 0.21 and newer, compatible with Scala 2.12 and above. [NR-325523](https://new-relic.atlassian.net/browse/NR-325523), [NR-325525](https://new-relic.atlassian.net/browse/NR-325525), [NR-293846](https://new-relic.atlassian.net/browse/NR-293846) +- [PR-228](https://github.com/newrelic/csec-java-agent/pull/228) HTTP4s Ember Client Support: Introduced support for HTTP4s Ember Client version 0.23 and above, compatible with Scala 2.12 and above. [NR-307676](https://new-relic.atlassian.net/browse/NR-307676) +- [PR-346](https://github.com/newrelic/csec-java-agent/pull/346) HTTP4s Blaze Client Support: Added support for HTTP4s Blaze Client version 0.21 and newer, compatible with Scala 2.12 and above. [NR-325526](https://new-relic.atlassian.net/browse/NR-325526), [NR-325527](https://new-relic.atlassian.net/browse/NR-325527) +- [PR-363](https://github.com/newrelic/csec-java-agent/pull/363) GraphQL Support: GraphQL support is now enabled by default. + +### Changes +- [PR-331](https://github.com/newrelic/csec-java-agent/pull/331) REST Client Update for IAST Request Replay: Migrated to utilize the Apache HTTP Client for enhanced request replay functionality. [NR-283130](https://new-relic.atlassian.net/browse/NR-283130) +- [PR-311](https://github.com/newrelic/csec-java-agent/pull/311) Status File Removed: The status file used for debugging has been eliminated. All debugging capabilities have been integrated into Init Logging or the Error Inbox. [NR-297214](https://new-relic.atlassian.net/browse/NR-297214) +- [PR-356](https://github.com/newrelic/csec-java-agent/pull/356) Code Optimization: Optimized code to minimize the overhead of the Security Agent in relation to the APM Agent. [NR-338596](https://new-relic.atlassian.net/browse/NR-338596) + +### Fixes +- [PR-352](https://github.com/newrelic/csec-java-agent/pull/352) Corrected the issue regarding inaccurate user class details in the mule-demo-app. [NR-336715](https://new-relic.atlassian.net/browse/NR-336715) +- [PR-355](https://github.com/newrelic/csec-java-agent/pull/355) Improved logging for scenarios where delay is set to a negative value. [NR-338578](https://new-relic.atlassian.net/browse/NR-338578) + + ## [1.5.1] - 2024-11-9 ### New features - [PR-350](https://github.com/newrelic/csec-java-agent/pull/350) IAST support for CI/CD. diff --git a/gradle.properties b/gradle.properties index e642d6bf5..220b28d15 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # The agent version. -agentVersion=1.5.1 +agentVersion=1.6.0 jsonVersion=1.2.9 # Updated exposed NR APM API version. nrAPIVersion=8.12.0 diff --git a/instrumentation-security/http4s-blaze-client-2.12_0.21/build.gradle b/instrumentation-security/http4s-blaze-client-2.12_0.21/build.gradle new file mode 100644 index 000000000..cc4ee7d23 --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.12_0.21/build.gradle @@ -0,0 +1,26 @@ +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-blaze-client_2.12:0.21.24') + implementation("org.typelevel:cats-effect_2.12:2.5.5") +} + +jar { + manifest { + attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.http4s-blaze-client-2.12_0.21', 'Priority': '-1' + } +} + +verifyInstrumentation { + passes 'org.http4s:http4s-blaze-client_2.12:[0.21,0.22)' + 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-blaze-client-2.12_0.21/src/main/java/org/http4s/BlazeClientBuilder_Instrumentation.java b/instrumentation-security/http4s-blaze-client-2.12_0.21/src/main/java/org/http4s/BlazeClientBuilder_Instrumentation.java new file mode 100644 index 000000000..a80d809da --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.12_0.21/src/main/java/org/http4s/BlazeClientBuilder_Instrumentation.java @@ -0,0 +1,22 @@ +package org.http4s; + +import cats.effect.ConcurrentEffect; +import cats.effect.Resource; +import com.newrelic.agent.security.instrumentation.http4s.blaze.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.client.blaze.BlazeClientBuilder") +public abstract class BlazeClientBuilder_Instrumentation { + + public ConcurrentEffect F() { + return Weaver.callOriginal(); + } + + public Resource> resource() { + Resource> delegateResource = Weaver.callOriginal(); + return NewrelicSecurityClientMiddleware$.MODULE$.resource(delegateResource, F()); + } +} diff --git a/instrumentation-security/http4s-blaze-client-2.12_0.21/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/NewrelicSecurityClientMiddleware.scala b/instrumentation-security/http4s-blaze-client-2.12_0.21/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/NewrelicSecurityClientMiddleware.scala new file mode 100644 index 000000000..cf63c2a81 --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.12_0.21/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/NewrelicSecurityClientMiddleware.scala @@ -0,0 +1,123 @@ +package com.newrelic.agent.security.instrumentation.http4s.blaze + +import cats.effect.{Async, ConcurrentEffect, 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-BLAZE-CLIENT-OUTBOUND" + private final val HTTP4S_BLAZE_CLIENT: String = "HTTP4S-BLAZE-CLIENT-2.12_0.21" + + private def construct[F[_] : Sync, T](t: T): F[T] = Sync[F].delay(t) + + private def clientResource[F[_] : ConcurrentEffect](client: Client[F]): Client[F] = + Client { req: Request[F] => + for { + // pre-process hook + operation <- Resource.liftF(construct { + val isLockAcquired = GenericHelper.acquireLockIfPossible(VulnerabilityCaseType.HTTP_REQUEST, nrSecCustomAttrName) + var operation: AbstractOperation = null + if (isLockAcquired) { + operation = preprocessSecurityHook(req) + } + operation + }) + + request <- Resource.liftF(construct {addSecurityHeaders(req, operation)}) + + // original call + response <- client.run(request) + + // post process and register exit event + newRes <- Resource.liftF(construct{ + val isLockAcquired = GenericHelper.isLockAcquired(nrSecCustomAttrName); + if (isLockAcquired) { + GenericHelper.releaseLock(nrSecCustomAttrName) + } + registerExitOperation(isLockAcquired, operation) + response + }) + + } yield newRes + } + + def resource[F[_] : ConcurrentEffect](delegate: Resource[F, Client[F]]): Resource[F, Client[F]] = { + val res: Resource[F, Client[F]] = delegate.map(c =>clientResource(c)) + res + } + + 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 + } + + + 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_BLAZE_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_BLAZE_CLIENT, e.getMessage), e, this.getClass.getName) + throw e + } + NewRelicSecurity.getAgent.log(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP4S_BLAZE_CLIENT, e.getMessage), e, this.getClass.getName) + NewRelicSecurity.getAgent.reportIncident(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP4S_BLAZE_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_BLAZE_CLIENT, e.getMessage), e, this.getClass.getName) + } + } +} + diff --git a/instrumentation-security/http4s-blaze-client-2.12_0.21/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/OutboundRequestWrapper.scala b/instrumentation-security/http4s-blaze-client-2.12_0.21/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/OutboundRequestWrapper.scala new file mode 100644 index 000000000..40a92e4f7 --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.12_0.21/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/OutboundRequestWrapper.scala @@ -0,0 +1,19 @@ +package com.newrelic.agent.security.instrumentation.http4s.blaze + +import org.http4s.util.CaseInsensitiveString +import org.http4s.{Header, Request} + +/** + * 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(CaseInsensitiveString.apply(key), value))) + } + def getRequest: Request[F] = { + req + } +} \ No newline at end of file diff --git a/instrumentation-security/http4s-blaze-client-2.12_0.21/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/BlazeClientTest.scala b/instrumentation-security/http4s-blaze-client-2.12_0.21/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/BlazeClientTest.scala new file mode 100644 index 000000000..8cc8bc228 --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.12_0.21/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/BlazeClientTest.scala @@ -0,0 +1,93 @@ +package com.nr.agent.security.instrumentation.blaze.client + +import cats.effect.{ConcurrentEffect, ContextShift, IO, Timer} +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.client.blaze.BlazeClientBuilder +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.ExecutionContext +import scala.concurrent.ExecutionContext.global +import scala.concurrent.duration.DurationInt + +@RunWith(classOf[SecurityInstrumentationTestRunner]) +@InstrumentationTestConfig(includePrefixes = Array("org.http4s", "com.newrelic.agent.security.instrumentation.http4s")) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class BlazeClientTest { + + @Rule + def server: HttpServerRule = httpServer + + implicit val ec: ExecutionContext = global + implicit val cs: ContextShift[IO] = IO.contextShift(global) + implicit val timer: Timer[IO] = IO.timer(global) + + val httpServer = new HttpServerRule() + + @Test + def blazeClientTest(): Unit = { + + val introspector: SecurityIntrospector = SecurityInstrumentationTestRunner.getIntrospector + makeRequest(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(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[_] : ContextShift : Timer](url: String)( + implicit ex: ExecutionContext, c: ConcurrentEffect[F]): F[String] = { + BlazeClientBuilder[F](ex).resource.use { client => + client.expect[String](url) + } + } +} + diff --git a/instrumentation-security/http4s-blaze-client-2.12_0.22/build.gradle b/instrumentation-security/http4s-blaze-client-2.12_0.22/build.gradle new file mode 100644 index 000000000..90a536b9c --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.12_0.22/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-blaze-client_2.12:0.22.14') + implementation("org.typelevel:cats-effect_2.12:2.5.5") +} + +jar { + manifest { + attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.http4s-blaze-client-2.12_0.22', 'Priority': '-1' + } +} + +verifyInstrumentation { + passes 'org.http4s:http4s-blaze-client_2.12:[0.22.0,0.23.0)' + excludeRegex '.*(RC|M)[0-9]*' + excludeRegex '.*0.22\\-[0-9].*' +} + +sourceSets.main.scala.srcDirs = ['src/main/scala', 'src/main/java'] +sourceSets.main.java.srcDirs = [] diff --git a/instrumentation-security/http4s-blaze-client-2.12_0.22/src/main/java/org/http4s/BlazeClientBuilder_Instrumentation.java b/instrumentation-security/http4s-blaze-client-2.12_0.22/src/main/java/org/http4s/BlazeClientBuilder_Instrumentation.java new file mode 100644 index 000000000..8159dc0be --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.12_0.22/src/main/java/org/http4s/BlazeClientBuilder_Instrumentation.java @@ -0,0 +1,22 @@ +package org.http4s; + +import cats.effect.ConcurrentEffect; +import cats.effect.Resource; +import com.newrelic.agent.security.instrumentation.http4s.blaze.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.blaze.client.BlazeClientBuilder") +public abstract class BlazeClientBuilder_Instrumentation { + + public ConcurrentEffect F() { + return Weaver.callOriginal(); + } + + public Resource> resource() { + Resource> delegateResource = Weaver.callOriginal(); + return NewrelicSecurityClientMiddleware$.MODULE$.resource(delegateResource, F()); + } +} diff --git a/instrumentation-security/http4s-blaze-client-2.12_0.22/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/NewrelicSecurityClientMiddleware.scala b/instrumentation-security/http4s-blaze-client-2.12_0.22/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/NewrelicSecurityClientMiddleware.scala new file mode 100644 index 000000000..c26aaa4d3 --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.12_0.22/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/NewrelicSecurityClientMiddleware.scala @@ -0,0 +1,123 @@ +package com.newrelic.agent.security.instrumentation.http4s.blaze + +import cats.effect.{Async, 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-BLAZE-CLIENT-OUTBOUND" + private final val HTTP4S_BLAZE_CLIENT: String = "HTTP4S-BLAZE-CLIENT-2.12_0.22" + + 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 + }) + + 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 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 + } + + 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_BLAZE_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_BLAZE_CLIENT, e.getMessage), e, this.getClass.getName) + throw e + } + NewRelicSecurity.getAgent.log(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP4S_BLAZE_CLIENT, e.getMessage), e, this.getClass.getName) + NewRelicSecurity.getAgent.reportIncident(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP4S_BLAZE_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_BLAZE_CLIENT, e.getMessage), e, this.getClass.getName) + } + } +} + diff --git a/instrumentation-security/http4s-blaze-client-2.12_0.22/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/OutboundRequestWrapper.scala b/instrumentation-security/http4s-blaze-client-2.12_0.22/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/OutboundRequestWrapper.scala new file mode 100644 index 000000000..8f30ab6d3 --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.12_0.22/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/OutboundRequestWrapper.scala @@ -0,0 +1,19 @@ +package com.newrelic.agent.security.instrumentation.http4s.blaze + +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-blaze-client-2.12_0.22/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/BlazeClientTest.scala b/instrumentation-security/http4s-blaze-client-2.12_0.22/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/BlazeClientTest.scala new file mode 100644 index 000000000..8cc8bc228 --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.12_0.22/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/BlazeClientTest.scala @@ -0,0 +1,93 @@ +package com.nr.agent.security.instrumentation.blaze.client + +import cats.effect.{ConcurrentEffect, ContextShift, IO, Timer} +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.client.blaze.BlazeClientBuilder +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.ExecutionContext +import scala.concurrent.ExecutionContext.global +import scala.concurrent.duration.DurationInt + +@RunWith(classOf[SecurityInstrumentationTestRunner]) +@InstrumentationTestConfig(includePrefixes = Array("org.http4s", "com.newrelic.agent.security.instrumentation.http4s")) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class BlazeClientTest { + + @Rule + def server: HttpServerRule = httpServer + + implicit val ec: ExecutionContext = global + implicit val cs: ContextShift[IO] = IO.contextShift(global) + implicit val timer: Timer[IO] = IO.timer(global) + + val httpServer = new HttpServerRule() + + @Test + def blazeClientTest(): Unit = { + + val introspector: SecurityIntrospector = SecurityInstrumentationTestRunner.getIntrospector + makeRequest(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(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[_] : ContextShift : Timer](url: String)( + implicit ex: ExecutionContext, c: ConcurrentEffect[F]): F[String] = { + BlazeClientBuilder[F](ex).resource.use { client => + client.expect[String](url) + } + } +} + diff --git a/instrumentation-security/http4s-blaze-client-2.12_0.23/build.gradle b/instrumentation-security/http4s-blaze-client-2.12_0.23/build.gradle new file mode 100644 index 000000000..0ad63331a --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.12_0.23/build.gradle @@ -0,0 +1,25 @@ +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-blaze-client_2.12:0.23.12') + implementation("org.typelevel:cats-effect_2.12:3.3.12") +} + +jar { + manifest { + attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.http4s-blaze-client-2.12_0.23', 'Priority': '-1' + } +} +verifyInstrumentation { + passes 'org.http4s:http4s-blaze-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-blaze-client-2.12_0.23/src/main/java/org/http4s/BlazeClientBuilder_Instrumentation.java b/instrumentation-security/http4s-blaze-client-2.12_0.23/src/main/java/org/http4s/BlazeClientBuilder_Instrumentation.java new file mode 100644 index 000000000..c5ebc74be --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.12_0.23/src/main/java/org/http4s/BlazeClientBuilder_Instrumentation.java @@ -0,0 +1,22 @@ +package org.http4s; + +import cats.effect.kernel.Async; +import cats.effect.kernel.Resource; +import com.newrelic.agent.security.instrumentation.http4s.blaze.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.blaze.client.BlazeClientBuilder") +public abstract class BlazeClientBuilder_Instrumentation { + + public Async F() { + return Weaver.callOriginal(); + } + + public Resource> resource() { + Resource> delegateResource = Weaver.callOriginal(); + return NewrelicSecurityClientMiddleware$.MODULE$.resource(delegateResource, F()); + } +} diff --git a/instrumentation-security/http4s-blaze-client-2.12_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/NewrelicSecurityClientMiddleware.scala b/instrumentation-security/http4s-blaze-client-2.12_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/NewrelicSecurityClientMiddleware.scala new file mode 100644 index 000000000..d111a785f --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.12_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/NewrelicSecurityClientMiddleware.scala @@ -0,0 +1,123 @@ +package com.newrelic.agent.security.instrumentation.http4s.blaze + +import cats.effect.{Async, 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-BLAZE-CLIENT-OUTBOUND" + private final val HTTP4S_BLAZE_CLIENT: String = "HTTP4S-BLAZE-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 + }) + + 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 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 + } + + 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_BLAZE_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_BLAZE_CLIENT, e.getMessage), e, this.getClass.getName) + throw e + } + NewRelicSecurity.getAgent.log(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP4S_BLAZE_CLIENT, e.getMessage), e, this.getClass.getName) + NewRelicSecurity.getAgent.reportIncident(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP4S_BLAZE_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_BLAZE_CLIENT, e.getMessage), e, this.getClass.getName) + } + } +} + diff --git a/instrumentation-security/http4s-blaze-client-2.12_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/OutboundRequestWrapper.scala b/instrumentation-security/http4s-blaze-client-2.12_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/OutboundRequestWrapper.scala new file mode 100644 index 000000000..8f30ab6d3 --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.12_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/OutboundRequestWrapper.scala @@ -0,0 +1,19 @@ +package com.newrelic.agent.security.instrumentation.http4s.blaze + +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-blaze-client-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/BlazeClientTest.scala b/instrumentation-security/http4s-blaze-client-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/BlazeClientTest.scala new file mode 100644 index 000000000..0f60d5313 --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/BlazeClientTest.scala @@ -0,0 +1,93 @@ +package com.nr.agent.security.instrumentation.blaze.client + +import cats.effect.IO +import cats.effect.kernel.Async +import cats.effect.unsafe.IORuntime +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.blaze.client.BlazeClientBuilder +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.ExecutionContext +import scala.concurrent.ExecutionContext.global +import scala.concurrent.duration.DurationInt + +@RunWith(classOf[SecurityInstrumentationTestRunner]) +@InstrumentationTestConfig(includePrefixes = Array("org.http4s", "com.newrelic.agent.security.instrumentation.http4s")) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class BlazeClientTest { + + @Rule + def server: HttpServerRule = httpServer + + implicit val ec: ExecutionContext = global + implicit val io: IORuntime = IORuntime.global + + 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)(implicit ex: ExecutionContext): F[String] = { + BlazeClientBuilder[F].resource.use { client => + client.expect[String](url) + } + } +} + diff --git a/instrumentation-security/http4s-blaze-client-2.13_0.21/build.gradle b/instrumentation-security/http4s-blaze-client-2.13_0.21/build.gradle new file mode 100644 index 000000000..cd6ab5f5f --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.13_0.21/build.gradle @@ -0,0 +1,26 @@ +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.typelevel:cats-effect_2.13:2.5.5") + implementation('org.http4s:http4s-blaze-client_2.13:0.21.24') +} + +jar { + manifest { + attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.http4s-blaze-client-2.13_0.21', 'Priority': '-1' + } +} + +verifyInstrumentation { + passes 'org.http4s:http4s-blaze-client_2.13:[0.21.0,0.22.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-blaze-client-2.13_0.21/src/main/java/org/http4s/BlazeClientBuilder_Instrumentation.java b/instrumentation-security/http4s-blaze-client-2.13_0.21/src/main/java/org/http4s/BlazeClientBuilder_Instrumentation.java new file mode 100644 index 000000000..a80d809da --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.13_0.21/src/main/java/org/http4s/BlazeClientBuilder_Instrumentation.java @@ -0,0 +1,22 @@ +package org.http4s; + +import cats.effect.ConcurrentEffect; +import cats.effect.Resource; +import com.newrelic.agent.security.instrumentation.http4s.blaze.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.client.blaze.BlazeClientBuilder") +public abstract class BlazeClientBuilder_Instrumentation { + + public ConcurrentEffect F() { + return Weaver.callOriginal(); + } + + public Resource> resource() { + Resource> delegateResource = Weaver.callOriginal(); + return NewrelicSecurityClientMiddleware$.MODULE$.resource(delegateResource, F()); + } +} diff --git a/instrumentation-security/http4s-blaze-client-2.13_0.21/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/NewrelicSecurityClientMiddleware.scala b/instrumentation-security/http4s-blaze-client-2.13_0.21/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/NewrelicSecurityClientMiddleware.scala new file mode 100644 index 000000000..989bf012e --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.13_0.21/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/NewrelicSecurityClientMiddleware.scala @@ -0,0 +1,123 @@ +package com.newrelic.agent.security.instrumentation.http4s.blaze + +import cats.effect.{Async, 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-BLAZE-CLIENT-OUTBOUND" + private final val HTTP4S_BLAZE_CLIENT: String = "HTTP4S-BLAZE-CLIENT-2.13_0.21" + + 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.liftF( + construct { + val isLockAcquired = GenericHelper.acquireLockIfPossible(VulnerabilityCaseType.HTTP_REQUEST, nrSecCustomAttrName) + var operation: AbstractOperation = null + if (isLockAcquired) { + operation = preprocessSecurityHook(req) + } + operation + }) + + request <- Resource.liftF(construct {addSecurityHeaders(req, operation)}) + + // original call + response <- client.run(request) + + // post process and register exit event + newRes <- Resource.liftF(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 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 + } + + 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_BLAZE_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_BLAZE_CLIENT, e.getMessage), e, this.getClass.getName) + throw e + } + NewRelicSecurity.getAgent.log(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP4S_BLAZE_CLIENT, e.getMessage), e, this.getClass.getName) + NewRelicSecurity.getAgent.reportIncident(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP4S_BLAZE_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_BLAZE_CLIENT, e.getMessage), e, this.getClass.getName) + } + } +} + diff --git a/instrumentation-security/http4s-blaze-client-2.13_0.21/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/OutboundRequestWrapper.scala b/instrumentation-security/http4s-blaze-client-2.13_0.21/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/OutboundRequestWrapper.scala new file mode 100644 index 000000000..40a92e4f7 --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.13_0.21/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/OutboundRequestWrapper.scala @@ -0,0 +1,19 @@ +package com.newrelic.agent.security.instrumentation.http4s.blaze + +import org.http4s.util.CaseInsensitiveString +import org.http4s.{Header, Request} + +/** + * 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(CaseInsensitiveString.apply(key), value))) + } + def getRequest: Request[F] = { + req + } +} \ No newline at end of file diff --git a/instrumentation-security/http4s-blaze-client-2.13_0.21/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/BlazeClientTest.scala b/instrumentation-security/http4s-blaze-client-2.13_0.21/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/BlazeClientTest.scala new file mode 100644 index 000000000..a73ddb232 --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.13_0.21/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/BlazeClientTest.scala @@ -0,0 +1,93 @@ +package com.nr.agent.security.instrumentation.blaze.client + +import cats.effect.{ConcurrentEffect, ContextShift, IO, Timer} +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.BlazeClientTest$.makeRequest +import org.http4s.client.blaze.BlazeClientBuilder +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.ExecutionContext +import scala.concurrent.ExecutionContext.global +import scala.concurrent.duration.DurationInt + +@RunWith(classOf[SecurityInstrumentationTestRunner]) +@InstrumentationTestConfig(includePrefixes = Array("org.http4s", "com.newrelic.agent.security.instrumentation.http4s")) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class BlazeClientTest { + + @Rule + def server: HttpServerRule = httpServer + + implicit val ec: ExecutionContext = global + implicit val cs: ContextShift[IO] = IO.contextShift(global) + implicit val timer: Timer[IO] = IO.timer(global) + + val httpServer = new HttpServerRule() + + @Test + def blazeClientTest(): Unit = { + + val introspector: SecurityIntrospector = SecurityInstrumentationTestRunner.getIntrospector + makeRequest(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(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 BlazeClientTest$ { + def makeRequest[F[_] : ContextShift : Timer](url: String)( + implicit ex: ExecutionContext, c: ConcurrentEffect[F]): F[String] = { + BlazeClientBuilder[F](ex).resource.use { client => + client.expect[String](url) + } + } +} + diff --git a/instrumentation-security/http4s-blaze-client-2.13_0.22/build.gradle b/instrumentation-security/http4s-blaze-client-2.13_0.22/build.gradle new file mode 100644 index 000000000..89d63f453 --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.13_0.22/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.typelevel:cats-effect_2.13:2.5.5") + implementation('org.http4s:http4s-blaze-client_2.13:0.22.14') +} + +jar { + manifest { + attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.http4s-blaze-client-2.13_0.22', 'Priority': '-1' + } +} + +verifyInstrumentation { + passes 'org.http4s:http4s-blaze-client_2.13:[0.22.0,0.23.0)' + excludeRegex '.*(RC|M)[0-9]*' + excludeRegex '.*0.22\\-[0-9].*' +} + +sourceSets.main.scala.srcDirs = ['src/main/scala', 'src/main/java'] +sourceSets.main.java.srcDirs = [] diff --git a/instrumentation-security/http4s-blaze-client-2.13_0.22/src/main/java/org/http4s/BlazeClientBuilder_Instrumentation.java b/instrumentation-security/http4s-blaze-client-2.13_0.22/src/main/java/org/http4s/BlazeClientBuilder_Instrumentation.java new file mode 100644 index 000000000..8159dc0be --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.13_0.22/src/main/java/org/http4s/BlazeClientBuilder_Instrumentation.java @@ -0,0 +1,22 @@ +package org.http4s; + +import cats.effect.ConcurrentEffect; +import cats.effect.Resource; +import com.newrelic.agent.security.instrumentation.http4s.blaze.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.blaze.client.BlazeClientBuilder") +public abstract class BlazeClientBuilder_Instrumentation { + + public ConcurrentEffect F() { + return Weaver.callOriginal(); + } + + public Resource> resource() { + Resource> delegateResource = Weaver.callOriginal(); + return NewrelicSecurityClientMiddleware$.MODULE$.resource(delegateResource, F()); + } +} diff --git a/instrumentation-security/http4s-blaze-client-2.13_0.22/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/NewrelicSecurityClientMiddleware.scala b/instrumentation-security/http4s-blaze-client-2.13_0.22/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/NewrelicSecurityClientMiddleware.scala new file mode 100644 index 000000000..425ee249c --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.13_0.22/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/NewrelicSecurityClientMiddleware.scala @@ -0,0 +1,123 @@ +package com.newrelic.agent.security.instrumentation.http4s.blaze + +import cats.effect.{Async, 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-BLAZE-CLIENT-OUTBOUND" + private final val HTTP4S_BLAZE_CLIENT: String = "HTTP4S-BLAZE-CLIENT-2.13_0.22" + + 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 + }) + + 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 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 + } + + 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_BLAZE_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_BLAZE_CLIENT, e.getMessage), e, this.getClass.getName) + throw e + } + NewRelicSecurity.getAgent.log(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP4S_BLAZE_CLIENT, e.getMessage), e, this.getClass.getName) + NewRelicSecurity.getAgent.reportIncident(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP4S_BLAZE_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_BLAZE_CLIENT, e.getMessage), e, this.getClass.getName) + } + } +} + diff --git a/instrumentation-security/http4s-blaze-client-2.13_0.22/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/OutboundRequestWrapper.scala b/instrumentation-security/http4s-blaze-client-2.13_0.22/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/OutboundRequestWrapper.scala new file mode 100644 index 000000000..8f30ab6d3 --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.13_0.22/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/OutboundRequestWrapper.scala @@ -0,0 +1,19 @@ +package com.newrelic.agent.security.instrumentation.http4s.blaze + +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-blaze-client-2.13_0.22/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/BlazeClientTest.scala b/instrumentation-security/http4s-blaze-client-2.13_0.22/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/BlazeClientTest.scala new file mode 100644 index 000000000..fe646f10b --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.13_0.22/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/BlazeClientTest.scala @@ -0,0 +1,93 @@ +package com.nr.agent.security.instrumentation.blaze.client + +import cats.effect.{ConcurrentEffect, ContextShift, IO, Timer} +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.blaze.client.BlazeClientBuilder +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.ExecutionContext +import scala.concurrent.ExecutionContext.global +import scala.concurrent.duration.DurationInt + +@RunWith(classOf[SecurityInstrumentationTestRunner]) +@InstrumentationTestConfig(includePrefixes = Array("org.http4s", "com.newrelic.agent.security.instrumentation.http4s")) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class BlazeClientTest { + + @Rule + def server: HttpServerRule = httpServer + + implicit val ec: ExecutionContext = global + implicit val cs: ContextShift[IO] = IO.contextShift(global) + implicit val timer: Timer[IO] = IO.timer(global) + + val httpServer = new HttpServerRule() + + @Test + def blazeClientTest(): Unit = { + + val introspector: SecurityIntrospector = SecurityInstrumentationTestRunner.getIntrospector + makeRequest(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(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[_] : ContextShift : Timer](url: String)( + implicit ex: ExecutionContext, c: ConcurrentEffect[F]): F[String] = { + BlazeClientBuilder[F](ex).resource.use { client => + client.expect[String](url) + } + } +} + diff --git a/instrumentation-security/http4s-blaze-client-2.13_0.23/build.gradle b/instrumentation-security/http4s-blaze-client-2.13_0.23/build.gradle new file mode 100644 index 000000000..4770da3ff --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.13_0.23/build.gradle @@ -0,0 +1,25 @@ +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-blaze-client_2.13:0.23.12') + implementation("org.typelevel:cats-effect_2.13:3.3.12") +} + +jar { + manifest { + attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.http4s-blaze-client-2.13_0.23', 'Priority': '-1' + } +} +verifyInstrumentation { + passes 'org.http4s:http4s-blaze-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-blaze-client-2.13_0.23/src/main/java/org/http4s/BlazeClientBuilder_Instrumentation.java b/instrumentation-security/http4s-blaze-client-2.13_0.23/src/main/java/org/http4s/BlazeClientBuilder_Instrumentation.java new file mode 100644 index 000000000..c5ebc74be --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.13_0.23/src/main/java/org/http4s/BlazeClientBuilder_Instrumentation.java @@ -0,0 +1,22 @@ +package org.http4s; + +import cats.effect.kernel.Async; +import cats.effect.kernel.Resource; +import com.newrelic.agent.security.instrumentation.http4s.blaze.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.blaze.client.BlazeClientBuilder") +public abstract class BlazeClientBuilder_Instrumentation { + + public Async F() { + return Weaver.callOriginal(); + } + + public Resource> resource() { + Resource> delegateResource = Weaver.callOriginal(); + return NewrelicSecurityClientMiddleware$.MODULE$.resource(delegateResource, F()); + } +} diff --git a/instrumentation-security/http4s-blaze-client-2.13_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/NewrelicSecurityClientMiddleware.scala b/instrumentation-security/http4s-blaze-client-2.13_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/NewrelicSecurityClientMiddleware.scala new file mode 100644 index 000000000..788343539 --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.13_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/NewrelicSecurityClientMiddleware.scala @@ -0,0 +1,123 @@ +package com.newrelic.agent.security.instrumentation.http4s.blaze + +import cats.effect.{Async, 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-BLAZE-CLIENT-OUTBOUND" + private final val HTTP4S_BLAZE_CLIENT: String = "HTTP4S-BLAZE-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 + }) + + 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 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 + } + + 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_BLAZE_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_BLAZE_CLIENT, e.getMessage), e, this.getClass.getName) + throw e + } + NewRelicSecurity.getAgent.log(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP4S_BLAZE_CLIENT, e.getMessage), e, this.getClass.getName) + NewRelicSecurity.getAgent.reportIncident(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP4S_BLAZE_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_BLAZE_CLIENT, e.getMessage), e, this.getClass.getName) + } + } +} + diff --git a/instrumentation-security/http4s-blaze-client-2.13_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/OutboundRequestWrapper.scala b/instrumentation-security/http4s-blaze-client-2.13_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/OutboundRequestWrapper.scala new file mode 100644 index 000000000..8f30ab6d3 --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.13_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/blaze/OutboundRequestWrapper.scala @@ -0,0 +1,19 @@ +package com.newrelic.agent.security.instrumentation.http4s.blaze + +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-blaze-client-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/BlazeClientTest.scala b/instrumentation-security/http4s-blaze-client-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/BlazeClientTest.scala new file mode 100644 index 000000000..0f60d5313 --- /dev/null +++ b/instrumentation-security/http4s-blaze-client-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/BlazeClientTest.scala @@ -0,0 +1,93 @@ +package com.nr.agent.security.instrumentation.blaze.client + +import cats.effect.IO +import cats.effect.kernel.Async +import cats.effect.unsafe.IORuntime +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.blaze.client.BlazeClientBuilder +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.ExecutionContext +import scala.concurrent.ExecutionContext.global +import scala.concurrent.duration.DurationInt + +@RunWith(classOf[SecurityInstrumentationTestRunner]) +@InstrumentationTestConfig(includePrefixes = Array("org.http4s", "com.newrelic.agent.security.instrumentation.http4s")) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class BlazeClientTest { + + @Rule + def server: HttpServerRule = httpServer + + implicit val ec: ExecutionContext = global + implicit val io: IORuntime = IORuntime.global + + 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)(implicit ex: ExecutionContext): F[String] = { + BlazeClientBuilder[F].resource.use { client => + client.expect[String](url) + } + } +} + diff --git a/instrumentation-security/http4s-blaze-server-2.12_0.21/build.gradle b/instrumentation-security/http4s-blaze-server-2.12_0.21/build.gradle new file mode 100644 index 000000000..74acd27fd --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.12_0.21/build.gradle @@ -0,0 +1,26 @@ +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-blaze-server_2.12:0.21.24') + testImplementation("org.http4s:http4s-dsl_2.12:0.21.24") +} + +jar { + manifest { + attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.http4s-blaze-server-2.12_0.21', 'Priority': '-1' + } +} + +verifyInstrumentation { + passes 'org.http4s:http4s-blaze-server_2.12:[0.21.0,0.22.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-blaze-server-2.12_0.21/src/main/java/org/http4s/server/blaze/BlazeServerBuilder_Instrumentation.java b/instrumentation-security/http4s-blaze-server-2.12_0.21/src/main/java/org/http4s/server/blaze/BlazeServerBuilder_Instrumentation.java new file mode 100644 index 000000000..e6fc9daa9 --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.12_0.21/src/main/java/org/http4s/server/blaze/BlazeServerBuilder_Instrumentation.java @@ -0,0 +1,20 @@ +package org.http4s.server.blaze; + +import cats.data.Kleisli; +import cats.effect.ConcurrentEffect; +import com.newrelic.agent.security.http4s.blaze.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.server.blaze.BlazeServerBuilder") +public class BlazeServerBuilder_Instrumentation { + + private final ConcurrentEffect F = Weaver.callOriginal(); + + public BlazeServerBuilder withHttpApp(Kleisli, Response> httpApp) { + httpApp = RequestProcessor$.MODULE$.genHttpApp(httpApp, this.F); + return Weaver.callOriginal(); + } +} diff --git a/instrumentation-security/http4s-blaze-server-2.12_0.21/src/main/scala/com/newrelic/agent/security/http4s/blaze/server/RequestProcessor.scala b/instrumentation-security/http4s-blaze-server-2.12_0.21/src/main/scala/com/newrelic/agent/security/http4s/blaze/server/RequestProcessor.scala new file mode 100644 index 000000000..ceb9aa337 --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.12_0.21/src/main/scala/com/newrelic/agent/security/http4s/blaze/server/RequestProcessor.scala @@ -0,0 +1,184 @@ +package com.newrelic.agent.security.http4s.blaze.server + +import cats.data.Kleisli +import cats.effect.Sync +import cats.implicits._ +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.utils.logging.LogLevel +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-BLAZE-SERVER-2.12_0.21" + 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-BLAZE-REQUEST_LOCK") + try { + if (NewRelicSecurity.isHookProcessingActive && 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.toInt) + securityRequest.setClientIP(request.remoteAddr.get.toString) + + securityRequest.setProtocol("http") + if (request.isSecure.get) { + securityRequest.setProtocol("https") + } + + 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, 2, 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 getContentType(headers: util.Map[String, String]): String = { + var contentType = StringUtils.EMPTY + if (headers.containsKey("content-type")) contentType = headers.get("content-type") + contentType + } + + private def processRequestHeaders(headers: Headers, securityRequest: HttpRequest): Unit = { + headers.foreach(header => { + var takeNextValue = false + var headerKey = StringUtils.EMPTY + if (header.name != null && !header.name.isEmpty) { + headerKey = header.name.toString + } + val headerValue = header.value + + val agentPolicy = NewRelicSecurity.getAgent.getCurrentPolicy + val 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 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.bodyAsText(defaultCharset = charset).compile.string + } else { + msg.bodyAsText.compile.string + } + } + + private def postProcessSecurityHook[F[_]: Sync](isLockAcquired:Boolean, response: Response[F], body: String): F[Unit] = construct { + try { + if (NewRelicSecurity.isHookProcessingActive && isLockAcquired && !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)) { + NewRelicSecurity.getAgent.getSecurityMetaData.getMetaData.setFromJumpRequiredInStackTrace(3) + 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 processResponseHeaders(headers: Headers, securityResp: HttpResponse): Unit = { + headers.foreach(header => { + if (header.name != null && !header.name.isEmpty) { + securityResp.getHeaders.put(header.name.toString.toLowerCase, header.value) + } + }) + } + + 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-blaze-server-2.12_0.21/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/BlazeServerBuilderTest.scala b/instrumentation-security/http4s-blaze-server-2.12_0.21/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/BlazeServerBuilderTest.scala new file mode 100644 index 000000000..00dc05603 --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.12_0.21/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/BlazeServerBuilderTest.scala @@ -0,0 +1,135 @@ +package com.nr.agent.security.instrumentation.blaze.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.util.CaseInsensitiveString +import org.http4s.{Header, HttpRoutes} +import org.junit.runner.RunWith +import org.junit.{After, Assert, Before, Test} + +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.blaze.server")) +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(CaseInsensitiveString.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.assertEquals("Invalid protocol.", "http", operation.getRequest.getProtocol) + 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-blaze-server-2.12_0.21/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/Http4sTestServer.scala b/instrumentation-security/http4s-blaze-server-2.12_0.21/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/Http4sTestServer.scala new file mode 100644 index 000000000..70cd5c070 --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.12_0.21/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/Http4sTestServer.scala @@ -0,0 +1,36 @@ +package com.nr.agent.security.instrumentation.blaze.server + +import scala.concurrent.ExecutionContext.global +import cats.effect.{ConcurrentEffect, ContextShift, IO, Resource, Timer} +import org.http4s.HttpApp +import org.http4s.server.blaze.BlazeServerBuilder +import org.http4s.server.Server + +import scala.concurrent.ExecutionContext + +class Http4sTestServer(val testServerHost: String, val port: Int, val httpApp: HttpApp[IO]) { + + var server: Server[IO] = _ + var finalizer: IO[Unit] = _ + + implicit val cs: ContextShift[IO] = IO.contextShift(global) + implicit val timer: Timer[IO] = IO.timer(global) + implicit val concurrentEffect: ConcurrentEffect[IO] = IO.ioConcurrentEffect + + implicit val ec: ExecutionContext = global + + val serverResource: Resource[IO, Server[IO]] = BlazeServerBuilder.apply(global) + .withHttpApp(httpApp) + .bindHttp(port, testServerHost) + .resource + + 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-blaze-server-2.12_0.22/build.gradle b/instrumentation-security/http4s-blaze-server-2.12_0.22/build.gradle new file mode 100644 index 000000000..0cbffcfaa --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.12_0.22/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-blaze-server_2.12:0.22.14') + testImplementation("org.http4s:http4s-dsl_2.12:0.22.14") +} + +jar { + manifest { + attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.http4s-blaze-server-2.12_0.22', 'Priority': '-1' + } +} + +verifyInstrumentation { + passes 'org.http4s:http4s-blaze-server_2.12:[0.22.0,0.23.0)' + excludeRegex '.*(RC|M)[0-9]*' + excludeRegex '.*0.22\\-[0-9].*' +} + +sourceSets.main.scala.srcDirs = ['src/main/scala', 'src/main/java'] +sourceSets.main.java.srcDirs = [] diff --git a/instrumentation-security/http4s-blaze-server-2.12_0.22/src/main/java/org/http4s/blaze/server/BlazeServerBuilder_Instrumentation.java b/instrumentation-security/http4s-blaze-server-2.12_0.22/src/main/java/org/http4s/blaze/server/BlazeServerBuilder_Instrumentation.java new file mode 100644 index 000000000..aa47a334a --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.12_0.22/src/main/java/org/http4s/blaze/server/BlazeServerBuilder_Instrumentation.java @@ -0,0 +1,20 @@ +package org.http4s.blaze.server; + +import cats.data.Kleisli; +import cats.effect.ConcurrentEffect; +import com.newrelic.agent.security.http4s.blaze.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.blaze.server.BlazeServerBuilder") +public class BlazeServerBuilder_Instrumentation { + + private final ConcurrentEffect F = Weaver.callOriginal(); + + public BlazeServerBuilder withHttpApp(Kleisli, Response> httpApp) { + httpApp = RequestProcessor$.MODULE$.genHttpApp(httpApp, this.F); + return Weaver.callOriginal(); + } +} diff --git a/instrumentation-security/http4s-blaze-server-2.12_0.22/src/main/scala/com/newrelic/agent/security/http4s/blaze/server/RequestProcessor.scala b/instrumentation-security/http4s-blaze-server-2.12_0.22/src/main/scala/com/newrelic/agent/security/http4s/blaze/server/RequestProcessor.scala new file mode 100644 index 000000000..5006fa96b --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.12_0.22/src/main/scala/com/newrelic/agent/security/http4s/blaze/server/RequestProcessor.scala @@ -0,0 +1,186 @@ +package com.newrelic.agent.security.http4s.blaze.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.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-BLAZE-SERVER-2.12_0.22" + 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-BLAZE-REQUEST_LOCK") + try { + if (NewRelicSecurity.isHookProcessingActive && 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) + + securityRequest.setProtocol("http") + if (request.isSecure.get) { + securityRequest.setProtocol("https") + } + + 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, 2, 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 getContentType(headers: util.Map[String, String]): String = { + var contentType = StringUtils.EMPTY + if (headers.containsKey("content-type")) contentType = headers.get("content-type") + contentType + } + + private def processRequestHeaders(headers: Headers, securityRequest: HttpRequest): Unit = { + headers.foreach(header => { + var takeNextValue = false + var headerKey = StringUtils.EMPTY + if (header.name != null && header.name.nonEmpty) { + headerKey = header.name.toString + } + val headerValue = header.value + + val agentPolicy = NewRelicSecurity.getAgent.getCurrentPolicy + val 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 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 (NewRelicSecurity.isHookProcessingActive && isLockAcquired && !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)) { + NewRelicSecurity.getAgent.getSecurityMetaData.getMetaData.setFromJumpRequiredInStackTrace(3) + 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 processResponseHeaders(headers: Headers, securityResp: HttpResponse): Unit = { + headers.foreach(header => { + if (header.name != null && !header.name.isEmpty) { + securityResp.getHeaders.put(header.name.toString.toLowerCase, header.value) + } + }) + } + + 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-blaze-server-2.12_0.22/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/BlazeServerBuilderTest.scala b/instrumentation-security/http4s-blaze-server-2.12_0.22/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/BlazeServerBuilderTest.scala new file mode 100644 index 000000000..3fc5dc675 --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.12_0.22/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/BlazeServerBuilderTest.scala @@ -0,0 +1,135 @@ +package com.nr.agent.security.instrumentation.blaze.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.blaze.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.assertEquals("Invalid protocol.", "http", operation.getRequest.getProtocol) + 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-blaze-server-2.12_0.22/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/Http4sTestServer.scala b/instrumentation-security/http4s-blaze-server-2.12_0.22/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/Http4sTestServer.scala new file mode 100644 index 000000000..479ba05f4 --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.12_0.22/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/Http4sTestServer.scala @@ -0,0 +1,36 @@ +package com.nr.agent.security.instrumentation.blaze.server + +import scala.concurrent.ExecutionContext.global +import cats.effect.{ConcurrentEffect, ContextShift, IO, Resource, Timer} +import org.http4s.HttpApp +import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.server.Server + +import scala.concurrent.ExecutionContext + +class Http4sTestServer(val testServerHost: String, val port: Int, val httpApp: HttpApp[IO]) { + + var server: Server = _ + var finalizer: IO[Unit] = _ + + implicit val cs: ContextShift[IO] = IO.contextShift(global) + implicit val timer: Timer[IO] = IO.timer(global) + implicit val concurrentEffect: ConcurrentEffect[IO] = IO.ioConcurrentEffect + + implicit val ec: ExecutionContext = global + + val serverResource: Resource[IO, Server] = BlazeServerBuilder.apply(global) + .withHttpApp(httpApp) + .bindHttp(port, testServerHost) + .resource + + 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-blaze-server-2.12_0.23/build.gradle b/instrumentation-security/http4s-blaze-server-2.12_0.23/build.gradle new file mode 100644 index 000000000..1c12651fb --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.12_0.23/build.gradle @@ -0,0 +1,26 @@ +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-blaze-server_2.12:0.23.12') + implementation("org.typelevel:cats-effect_2.12:3.3.12") + testImplementation("org.http4s:http4s-dsl_2.12:0.23.12") +} + +jar { + manifest { + attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.http4s-blaze-server-2.12_0.23', 'Priority': '-1' + } +} +verifyInstrumentation { + passes 'org.http4s:http4s-blaze-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-blaze-server-2.12_0.23/src/main/java/org/http4s/blaze/server/BlazeServerBuilder_Instrumentation.java b/instrumentation-security/http4s-blaze-server-2.12_0.23/src/main/java/org/http4s/blaze/server/BlazeServerBuilder_Instrumentation.java new file mode 100644 index 000000000..50298f8b6 --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.12_0.23/src/main/java/org/http4s/blaze/server/BlazeServerBuilder_Instrumentation.java @@ -0,0 +1,20 @@ +package org.http4s.blaze.server; + +import cats.data.Kleisli; +import cats.effect.kernel.Async; +import com.newrelic.agent.security.http4s.blaze.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.blaze.server.BlazeServerBuilder") +public class BlazeServerBuilder_Instrumentation { + + private final Async F = Weaver.callOriginal(); + + public BlazeServerBuilder withHttpApp(Kleisli, Response> httpApp) { + httpApp = RequestProcessor$.MODULE$.genHttpApp(httpApp, this.F); + return Weaver.callOriginal(); + } +} diff --git a/instrumentation-security/http4s-blaze-server-2.12_0.23/src/main/scala/com/newrelic/agent/security/http4s/blaze/server/RequestProcessor.scala b/instrumentation-security/http4s-blaze-server-2.12_0.23/src/main/scala/com/newrelic/agent/security/http4s/blaze/server/RequestProcessor.scala new file mode 100644 index 000000000..f96537f72 --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.12_0.23/src/main/scala/com/newrelic/agent/security/http4s/blaze/server/RequestProcessor.scala @@ -0,0 +1,186 @@ +package com.newrelic.agent.security.http4s.blaze.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.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-BLAZE-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-BLAZE-REQUEST_LOCK") + try { + if (NewRelicSecurity.isHookProcessingActive && 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) + + securityRequest.setProtocol("http") + if (request.isSecure.get) { + securityRequest.setProtocol("https") + } + + 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, 2, 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 getContentType(headers: util.Map[String, String]): String = { + var contentType = StringUtils.EMPTY + if (headers.containsKey("content-type")) contentType = headers.get("content-type") + contentType + } + + private def processRequestHeaders(headers: Headers, securityRequest: HttpRequest): Unit = { + headers.foreach(header => { + var takeNextValue = false + var headerKey = StringUtils.EMPTY + if (header.name != null && header.name.nonEmpty) { + headerKey = header.name.toString + } + val headerValue = header.value + + val agentPolicy = NewRelicSecurity.getAgent.getCurrentPolicy + val 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 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 (NewRelicSecurity.isHookProcessingActive && isLockAcquired && !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)) { + NewRelicSecurity.getAgent.getSecurityMetaData.getMetaData.setFromJumpRequiredInStackTrace(3) + 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 processResponseHeaders(headers: Headers, securityResp: HttpResponse): Unit = { + headers.foreach(header => { + if (header.name != null && !header.name.isEmpty) { + securityResp.getHeaders.put(header.name.toString.toLowerCase, header.value) + } + }) + } + + 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-blaze-server-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/BlazeServerBuilderTest.scala b/instrumentation-security/http4s-blaze-server-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/BlazeServerBuilderTest.scala new file mode 100644 index 000000000..3fc5dc675 --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/BlazeServerBuilderTest.scala @@ -0,0 +1,135 @@ +package com.nr.agent.security.instrumentation.blaze.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.blaze.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.assertEquals("Invalid protocol.", "http", operation.getRequest.getProtocol) + 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-blaze-server-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/Http4sTestServer.scala b/instrumentation-security/http4s-blaze-server-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/Http4sTestServer.scala new file mode 100644 index 000000000..182bec092 --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/Http4sTestServer.scala @@ -0,0 +1,28 @@ +package com.nr.agent.security.instrumentation.blaze.server + +import cats.effect.unsafe.implicits.global +import cats.effect.{IO, Resource} +import org.http4s.HttpApp +import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.server.Server + +class Http4sTestServer(val testServerHost: String, val port: Int, val httpApp: HttpApp[IO]) { + + var server: Server = _ + var finalizer: IO[Unit] = _ + + val serverResource: Resource[IO, Server] = BlazeServerBuilder[IO] + .withHttpApp(httpApp) + .bindHttp(port, testServerHost) + .resource + + 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-blaze-server-2.13_0.21/build.gradle b/instrumentation-security/http4s-blaze-server-2.13_0.21/build.gradle new file mode 100644 index 000000000..0f291eb00 --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.13_0.21/build.gradle @@ -0,0 +1,26 @@ +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-blaze-server_2.13:0.21.24') + testImplementation("org.http4s:http4s-dsl_2.13:0.21.24") +} + +jar { + manifest { + attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.http4s-blaze-server-2.13_0.21', 'Priority': '-1' + } +} + +verifyInstrumentation { + passes 'org.http4s:http4s-blaze-server_2.13:[0.21.0,0.22.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-blaze-server-2.13_0.21/src/main/java/org/http4s/server/blaze/BlazeServerBuilder_Instrumentation.java b/instrumentation-security/http4s-blaze-server-2.13_0.21/src/main/java/org/http4s/server/blaze/BlazeServerBuilder_Instrumentation.java new file mode 100644 index 000000000..e6fc9daa9 --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.13_0.21/src/main/java/org/http4s/server/blaze/BlazeServerBuilder_Instrumentation.java @@ -0,0 +1,20 @@ +package org.http4s.server.blaze; + +import cats.data.Kleisli; +import cats.effect.ConcurrentEffect; +import com.newrelic.agent.security.http4s.blaze.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.server.blaze.BlazeServerBuilder") +public class BlazeServerBuilder_Instrumentation { + + private final ConcurrentEffect F = Weaver.callOriginal(); + + public BlazeServerBuilder withHttpApp(Kleisli, Response> httpApp) { + httpApp = RequestProcessor$.MODULE$.genHttpApp(httpApp, this.F); + return Weaver.callOriginal(); + } +} diff --git a/instrumentation-security/http4s-blaze-server-2.13_0.21/src/main/scala/com/newrelic/agent/security/http4s/blaze/server/RequestProcessor.scala b/instrumentation-security/http4s-blaze-server-2.13_0.21/src/main/scala/com/newrelic/agent/security/http4s/blaze/server/RequestProcessor.scala new file mode 100644 index 000000000..5d249bf6e --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.13_0.21/src/main/scala/com/newrelic/agent/security/http4s/blaze/server/RequestProcessor.scala @@ -0,0 +1,183 @@ +package com.newrelic.agent.security.http4s.blaze.server + +import cats.data.Kleisli +import cats.effect.Sync +import cats.implicits._ +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.utils.logging.LogLevel +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-BLAZE-SERVER-2.13_0.21" + 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-BLAZE-REQUEST_LOCK") + try { + if (NewRelicSecurity.isHookProcessingActive && 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.toInt) + securityRequest.setClientIP(request.remoteAddr.get.toString) + + securityRequest.setProtocol("http") + if (request.isSecure.get) { + securityRequest.setProtocol("https") + } + + 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, 2, 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 getContentType(headers: util.Map[String, String]): String = { + var contentType = StringUtils.EMPTY + if (headers.containsKey("content-type")) contentType = headers.get("content-type") + contentType + } + + private def processRequestHeaders(headers: Headers, securityRequest: HttpRequest): Unit = { + headers.foreach(header => { + var takeNextValue = false + var headerKey = StringUtils.EMPTY + if (header.name != null && !header.name.isEmpty) { + headerKey = header.name.toString + } + val headerValue = header.value + + val agentPolicy = NewRelicSecurity.getAgent.getCurrentPolicy + val 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 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.bodyAsText(defaultCharset = charset).compile.string + } else { + msg.bodyAsText.compile.string + } + } + + private def postProcessSecurityHook[F[_]: Sync](isLockAcquired:Boolean, response: Response[F], body: String): F[Unit] = construct { + try { + if (NewRelicSecurity.isHookProcessingActive && isLockAcquired && !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)) { + NewRelicSecurity.getAgent.getSecurityMetaData.getMetaData.setFromJumpRequiredInStackTrace(3) + 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 processResponseHeaders(headers: Headers, securityResp: HttpResponse): Unit = { + headers.foreach(header => { + if (header.name != null && !header.name.isEmpty) { + securityResp.getHeaders.put(header.name.toString.toLowerCase, header.value) + } + }) + } + + 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-blaze-server-2.13_0.21/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/BlazeServerBuilderTest.scala b/instrumentation-security/http4s-blaze-server-2.13_0.21/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/BlazeServerBuilderTest.scala new file mode 100644 index 000000000..00dc05603 --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.13_0.21/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/BlazeServerBuilderTest.scala @@ -0,0 +1,135 @@ +package com.nr.agent.security.instrumentation.blaze.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.util.CaseInsensitiveString +import org.http4s.{Header, HttpRoutes} +import org.junit.runner.RunWith +import org.junit.{After, Assert, Before, Test} + +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.blaze.server")) +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(CaseInsensitiveString.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.assertEquals("Invalid protocol.", "http", operation.getRequest.getProtocol) + 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-blaze-server-2.13_0.21/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/Http4sTestServer.scala b/instrumentation-security/http4s-blaze-server-2.13_0.21/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/Http4sTestServer.scala new file mode 100644 index 000000000..70cd5c070 --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.13_0.21/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/Http4sTestServer.scala @@ -0,0 +1,36 @@ +package com.nr.agent.security.instrumentation.blaze.server + +import scala.concurrent.ExecutionContext.global +import cats.effect.{ConcurrentEffect, ContextShift, IO, Resource, Timer} +import org.http4s.HttpApp +import org.http4s.server.blaze.BlazeServerBuilder +import org.http4s.server.Server + +import scala.concurrent.ExecutionContext + +class Http4sTestServer(val testServerHost: String, val port: Int, val httpApp: HttpApp[IO]) { + + var server: Server[IO] = _ + var finalizer: IO[Unit] = _ + + implicit val cs: ContextShift[IO] = IO.contextShift(global) + implicit val timer: Timer[IO] = IO.timer(global) + implicit val concurrentEffect: ConcurrentEffect[IO] = IO.ioConcurrentEffect + + implicit val ec: ExecutionContext = global + + val serverResource: Resource[IO, Server[IO]] = BlazeServerBuilder.apply(global) + .withHttpApp(httpApp) + .bindHttp(port, testServerHost) + .resource + + 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-blaze-server-2.13_0.22/build.gradle b/instrumentation-security/http4s-blaze-server-2.13_0.22/build.gradle new file mode 100644 index 000000000..eecd9a894 --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.13_0.22/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-blaze-server_2.13:0.22.14') + testImplementation("org.http4s:http4s-dsl_2.13:0.22.12") +} + +jar { + manifest { + attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.http4s-blaze-server-2.13_0.22', 'Priority': '-1' + } +} + +verifyInstrumentation { + passes 'org.http4s:http4s-blaze-server_2.13:[0.22.0,0.23.0)' + excludeRegex '.*(RC|M)[0-9]*' + excludeRegex '.*0.22\\-[0-9].*' +} + +sourceSets.main.scala.srcDirs = ['src/main/scala', 'src/main/java'] +sourceSets.main.java.srcDirs = [] diff --git a/instrumentation-security/http4s-blaze-server-2.13_0.22/src/main/java/org/http4s/blaze/server/BlazeServerBuilder_Instrumentation.java b/instrumentation-security/http4s-blaze-server-2.13_0.22/src/main/java/org/http4s/blaze/server/BlazeServerBuilder_Instrumentation.java new file mode 100644 index 000000000..aa47a334a --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.13_0.22/src/main/java/org/http4s/blaze/server/BlazeServerBuilder_Instrumentation.java @@ -0,0 +1,20 @@ +package org.http4s.blaze.server; + +import cats.data.Kleisli; +import cats.effect.ConcurrentEffect; +import com.newrelic.agent.security.http4s.blaze.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.blaze.server.BlazeServerBuilder") +public class BlazeServerBuilder_Instrumentation { + + private final ConcurrentEffect F = Weaver.callOriginal(); + + public BlazeServerBuilder withHttpApp(Kleisli, Response> httpApp) { + httpApp = RequestProcessor$.MODULE$.genHttpApp(httpApp, this.F); + return Weaver.callOriginal(); + } +} diff --git a/instrumentation-security/http4s-blaze-server-2.13_0.22/src/main/scala/com/newrelic/agent/security/http4s/blaze/server/RequestProcessor.scala b/instrumentation-security/http4s-blaze-server-2.13_0.22/src/main/scala/com/newrelic/agent/security/http4s/blaze/server/RequestProcessor.scala new file mode 100644 index 000000000..9186f4613 --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.13_0.22/src/main/scala/com/newrelic/agent/security/http4s/blaze/server/RequestProcessor.scala @@ -0,0 +1,186 @@ +package com.newrelic.agent.security.http4s.blaze.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.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-BLAZE-SERVER-2.13_0.22" + 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-BLAZE-REQUEST_LOCK") + try { + if (NewRelicSecurity.isHookProcessingActive && 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) + + securityRequest.setProtocol("http") + if (request.isSecure.get) { + securityRequest.setProtocol("https") + } + + 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, 2, 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 getContentType(headers: util.Map[String, String]): String = { + var contentType = StringUtils.EMPTY + if (headers.containsKey("content-type")) contentType = headers.get("content-type") + contentType + } + + private def processRequestHeaders(headers: Headers, securityRequest: HttpRequest): Unit = { + headers.foreach(header => { + var takeNextValue = false + var headerKey = StringUtils.EMPTY + if (header.name != null && header.name.nonEmpty) { + headerKey = header.name.toString + } + val headerValue = header.value + + val agentPolicy = NewRelicSecurity.getAgent.getCurrentPolicy + val 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 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 (NewRelicSecurity.isHookProcessingActive && isLockAcquired && !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.getSecurityMetaData.getMetaData.setFromJumpRequiredInStackTrace(3) + 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 processResponseHeaders(headers: Headers, securityResp: HttpResponse): Unit = { + headers.foreach(header => { + if (header.name != null && !header.name.isEmpty) { + securityResp.getHeaders.put(header.name.toString.toLowerCase, header.value) + } + }) + } + + 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-blaze-server-2.13_0.22/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/BlazeServerBuilderTest.scala b/instrumentation-security/http4s-blaze-server-2.13_0.22/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/BlazeServerBuilderTest.scala new file mode 100644 index 000000000..72aa40728 --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.13_0.22/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/BlazeServerBuilderTest.scala @@ -0,0 +1,135 @@ +package com.nr.agent.security.instrumentation.blaze.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.blaze.server")) +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.assertEquals("Invalid protocol.", "http", operation.getRequest.getProtocol) + 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-blaze-server-2.13_0.22/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/Http4sTestServer.scala b/instrumentation-security/http4s-blaze-server-2.13_0.22/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/Http4sTestServer.scala new file mode 100644 index 000000000..479ba05f4 --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.13_0.22/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/Http4sTestServer.scala @@ -0,0 +1,36 @@ +package com.nr.agent.security.instrumentation.blaze.server + +import scala.concurrent.ExecutionContext.global +import cats.effect.{ConcurrentEffect, ContextShift, IO, Resource, Timer} +import org.http4s.HttpApp +import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.server.Server + +import scala.concurrent.ExecutionContext + +class Http4sTestServer(val testServerHost: String, val port: Int, val httpApp: HttpApp[IO]) { + + var server: Server = _ + var finalizer: IO[Unit] = _ + + implicit val cs: ContextShift[IO] = IO.contextShift(global) + implicit val timer: Timer[IO] = IO.timer(global) + implicit val concurrentEffect: ConcurrentEffect[IO] = IO.ioConcurrentEffect + + implicit val ec: ExecutionContext = global + + val serverResource: Resource[IO, Server] = BlazeServerBuilder.apply(global) + .withHttpApp(httpApp) + .bindHttp(port, testServerHost) + .resource + + 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-blaze-server-2.13_0.23/build.gradle b/instrumentation-security/http4s-blaze-server-2.13_0.23/build.gradle new file mode 100644 index 000000000..1dadbfc1e --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.13_0.23/build.gradle @@ -0,0 +1,26 @@ +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-blaze-server_2.13:0.23.12') + implementation("org.typelevel:cats-effect_2.13:3.3.12") + testImplementation("org.http4s:http4s-dsl_2.13:0.23.12") +} + +jar { + manifest { + attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.http4s-blaze-server-2.13_0.23', 'Priority': '-1' + } +} +verifyInstrumentation { + passes 'org.http4s:http4s-blaze-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-blaze-server-2.13_0.23/src/main/java/org/http4s/blaze/server/BlazeServerBuilder_Instrumentation.java b/instrumentation-security/http4s-blaze-server-2.13_0.23/src/main/java/org/http4s/blaze/server/BlazeServerBuilder_Instrumentation.java new file mode 100644 index 000000000..50298f8b6 --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.13_0.23/src/main/java/org/http4s/blaze/server/BlazeServerBuilder_Instrumentation.java @@ -0,0 +1,20 @@ +package org.http4s.blaze.server; + +import cats.data.Kleisli; +import cats.effect.kernel.Async; +import com.newrelic.agent.security.http4s.blaze.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.blaze.server.BlazeServerBuilder") +public class BlazeServerBuilder_Instrumentation { + + private final Async F = Weaver.callOriginal(); + + public BlazeServerBuilder withHttpApp(Kleisli, Response> httpApp) { + httpApp = RequestProcessor$.MODULE$.genHttpApp(httpApp, this.F); + return Weaver.callOriginal(); + } +} diff --git a/instrumentation-security/http4s-blaze-server-2.13_0.23/src/main/scala/com/newrelic/agent/security/http4s/blaze/server/RequestProcessor.scala b/instrumentation-security/http4s-blaze-server-2.13_0.23/src/main/scala/com/newrelic/agent/security/http4s/blaze/server/RequestProcessor.scala new file mode 100644 index 000000000..bbcc5281b --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.13_0.23/src/main/scala/com/newrelic/agent/security/http4s/blaze/server/RequestProcessor.scala @@ -0,0 +1,187 @@ +package com.newrelic.agent.security.http4s.blaze.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.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-BLAZE-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-BLAZE-REQUEST_LOCK") + try { + if (NewRelicSecurity.isHookProcessingActive && 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) + + securityRequest.setProtocol("http") + if (request.isSecure.get) { + securityRequest.setProtocol("https") + } + + 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, 2, 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 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 getContentType(headers: util.Map[String, String]): String = { + var contentType = StringUtils.EMPTY + if (headers.containsKey("content-type")) contentType = headers.get("content-type") + contentType + } + + private def processRequestHeaders(headers: Headers, securityRequest: HttpRequest): Unit = { + headers.foreach(header => { + var takeNextValue = false + var headerKey = StringUtils.EMPTY + if (header.name != null && header.name.nonEmpty) { + headerKey = header.name.toString + } + val headerValue = header.value + + val agentPolicy = NewRelicSecurity.getAgent.getCurrentPolicy + val 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 postProcessSecurityHook[F[_]: Sync](isLockAcquired:Boolean, response: Response[F], body: String): F[Unit] = construct { + try { + if (NewRelicSecurity.isHookProcessingActive && isLockAcquired && !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)) { + NewRelicSecurity.getAgent.getSecurityMetaData.getMetaData.setFromJumpRequiredInStackTrace(3) + 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 processResponseHeaders(headers: Headers, securityResp: HttpResponse): Unit = { + headers.foreach(header => { + if (header.name != null && !header.name.isEmpty) { + securityResp.getHeaders.put(header.name.toString.toLowerCase, header.value) + } + }) + } + + 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-blaze-server-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/BlazeServerBuilderTest.scala b/instrumentation-security/http4s-blaze-server-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/BlazeServerBuilderTest.scala new file mode 100644 index 000000000..3fc5dc675 --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/BlazeServerBuilderTest.scala @@ -0,0 +1,135 @@ +package com.nr.agent.security.instrumentation.blaze.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.blaze.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.assertEquals("Invalid protocol.", "http", operation.getRequest.getProtocol) + 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-blaze-server-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/Http4sTestServer.scala b/instrumentation-security/http4s-blaze-server-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/Http4sTestServer.scala new file mode 100644 index 000000000..182bec092 --- /dev/null +++ b/instrumentation-security/http4s-blaze-server-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/server/Http4sTestServer.scala @@ -0,0 +1,28 @@ +package com.nr.agent.security.instrumentation.blaze.server + +import cats.effect.unsafe.implicits.global +import cats.effect.{IO, Resource} +import org.http4s.HttpApp +import org.http4s.blaze.server.BlazeServerBuilder +import org.http4s.server.Server + +class Http4sTestServer(val testServerHost: String, val port: Int, val httpApp: HttpApp[IO]) { + + var server: Server = _ + var finalizer: IO[Unit] = _ + + val serverResource: Resource[IO, Server] = BlazeServerBuilder[IO] + .withHttpApp(httpApp) + .bindHttp(port, testServerHost) + .resource + + 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/mule-3.6/src/main/java/com/newrelic/agent/security/instrumentation/mule36/MuleHelper.java b/instrumentation-security/mule-3.6/src/main/java/com/newrelic/agent/security/instrumentation/mule36/MuleHelper.java index 386b7a75c..40dfc179c 100644 --- a/instrumentation-security/mule-3.6/src/main/java/com/newrelic/agent/security/instrumentation/mule36/MuleHelper.java +++ b/instrumentation-security/mule-3.6/src/main/java/com/newrelic/agent/security/instrumentation/mule36/MuleHelper.java @@ -120,9 +120,7 @@ public static void gatherURLMappings(HttpListener messageSource, Listcreate() .register("http", PlainConnectionSocketFactory.getSocketFactory()) .register("https", sslContext != null ? - new SSLConnectionSocketFactory(sslContext) : SSLConnectionSocketFactory.getSocketFactory()) + new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE) : SSLConnectionSocketFactory.getSocketFactory()) .build()); // We only allow one connection at a time to the backend. @@ -233,26 +235,18 @@ public ReadResult execute(HttpRequest httpRequest, String endpoint, String fuzzR } catch (IOException hostConnectException) { String message = "IOException Error while executing request %s: %s message : %s"; logger.log(LogLevel.FINE, String.format(message, fuzzRequestId, request, hostConnectException.getMessage()), ApacheHttpClientWrapper.class.getName()); - logger.postLogMessageIfNecessary(LogLevel.WARNING, String.format(message, request, hostConnectException.getMessage()), hostConnectException, ApacheHttpClientWrapper.class.getName()); + logger.postLogMessageIfNecessary(LogLevel.WARNING, String.format(message, fuzzRequestId, request, hostConnectException.getMessage()), hostConnectException, ApacheHttpClientWrapper.class.getName()); throw hostConnectException; } } private HttpUriRequest buildIastFuzzRequest(HttpRequest httpRequest, String endpoint, boolean addEventIgnoreHeader) throws URISyntaxException, UnsupportedEncodingException, ApacheHttpExceptionWrapper { RequestBuilder requestBuilder = getRequestBuilder(httpRequest.getMethod()); - URIBuilder uriBuilder = new URIBuilder(endpoint); String requestUrl = httpRequest.getUrl(); if (StringUtils.isBlank(requestUrl)) { throw new ApacheHttpExceptionWrapper("Request URL is empty"); } - String path = StringUtils.substringBefore(requestUrl, SEPARATOR_QUESTION_MARK); - uriBuilder.setPath(path); - String queryString = StringUtils.substringAfter(requestUrl, SEPARATOR_QUESTION_MARK); - if (StringUtils.isNotBlank(queryString)) { - //Use of this deprecated method is intentional as we are building the query string exactly as provided by SE. - uriBuilder.setQuery(queryString); - } - requestBuilder.setUri(uriBuilder.build()); + requestBuilder.setUri(createURL(endpoint, requestUrl)); if(StringUtils.startsWith(httpRequest.getContentType(), APPLICATION_X_WWW_FORM_URLENCODED)){ requestBuilder.setEntity(new UrlEncodedFormEntity(buildFormParameters(httpRequest.getParameterMap()))); } @@ -268,6 +262,19 @@ private HttpUriRequest buildIastFuzzRequest(HttpRequest httpRequest, String endp return requestBuilder.build(); } + private URI createURL(String endpoint, String requestUrl) { + if (StringUtils.isBlank(requestUrl)) { + return URI.create(endpoint); + } + if (StringUtils.endsWith(endpoint, SUFFIX_SLASH) && StringUtils.startsWith(requestUrl, SUFFIX_SLASH)) { + return URI.create(endpoint + requestUrl.substring(1)); + } else if (StringUtils.endsWith(endpoint, SUFFIX_SLASH) || StringUtils.startsWith(requestUrl, SUFFIX_SLASH)) { + return URI.create(endpoint + requestUrl); + } else { + return URI.create(endpoint + SUFFIX_SLASH + requestUrl); + } + } + private List buildFormParameters(Map parameterMap) { List formParameters = new ArrayList<>(); for (Map.Entry formData : parameterMap.entrySet()) { diff --git a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/apache/httpclient/IastHttpClient.java b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/apache/httpclient/IastHttpClient.java index 890f4b944..97130132d 100644 --- a/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/apache/httpclient/IastHttpClient.java +++ b/newrelic-security-agent/src/main/java/com/newrelic/agent/security/intcodeagent/apache/httpclient/IastHttpClient.java @@ -77,7 +77,10 @@ public void tryToEstablishApplicationEndpoint(HttpRequest request) { for (Map.Entry endpoint : endpoints.entrySet()) { try { ReadResult result = httpClient.execute(request, endpoint.getValue(), null, true); - if(result.getStatusCode() >= 200 && result.getStatusCode() <= 500) { + int statusCode = result.getStatusCode(); + if ((statusCode >= 200 && statusCode < 300) || + statusCode == 401 || statusCode == 402 || + statusCode == 406 || statusCode == 409) { ServerConnectionConfiguration serverConnectionConfiguration = new ServerConnectionConfiguration(serverPort, endpoint.getKey(), endpoint.getValue(), true); AppServerInfo appServerInfo = AppServerInfoHelper.getAppServerInfo(); appServerInfo.getConnectionConfiguration().put(serverPort, serverConnectionConfiguration); 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 6e3885c64..a9b9de952 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 @@ -889,7 +889,9 @@ public void setApplicationConnectionConfig(int port, String scheme) { AppServerInfo appServerInfo = AppServerInfoHelper.getAppServerInfo(); ServerConnectionConfiguration serverConnectionConfiguration = new ServerConnectionConfiguration(port, scheme); appServerInfo.getConnectionConfiguration().put(port, serverConnectionConfiguration); - logger.log(LogLevel.FINER, String.format("Unconfirmed connection configuration for port %d and scheme %s added.", port, scheme), this.getClass().getName()); + if(logger != null) { + logger.log(LogLevel.FINER, String.format("Unconfirmed connection configuration for port %d and scheme %s added.", port, scheme), this.getClass().getName()); + } // verifyConnectionAndPut(port, scheme, appServerInfo); } diff --git a/settings.gradle b/settings.gradle index ab12bfd9e..afec08f66 100644 --- a/settings.gradle +++ b/settings.gradle @@ -218,6 +218,18 @@ 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:http4s-blaze-server-2.12_0.21' +include 'instrumentation:http4s-blaze-server-2.12_0.22' +include 'instrumentation:http4s-blaze-server-2.12_0.23' +include 'instrumentation:http4s-blaze-server-2.13_0.21' +include 'instrumentation:http4s-blaze-server-2.13_0.22' +include 'instrumentation:http4s-blaze-server-2.13_0.23' +include 'instrumentation:http4s-blaze-client-2.12_0.21' +include 'instrumentation:http4s-blaze-client-2.12_0.22' +include 'instrumentation:http4s-blaze-client-2.12_0.23' +include 'instrumentation:http4s-blaze-client-2.13_0.21' +include 'instrumentation:http4s-blaze-client-2.13_0.22' +include 'instrumentation:http4s-blaze-client-2.13_0.23' 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'