From 14383d42b9e14083313e2d7fccc6a30d1f419c99 Mon Sep 17 00:00:00 2001 From: idawda Date: Thu, 11 Apr 2024 12:14:39 +0530 Subject: [PATCH 01/14] Initial PoC for http4s-ember-server-2.13 Co-authored-by: Lovesh Baya --- .../build.gradle | 27 ++++++ .../EmberServerBuilder_Instrumentation.java | 24 +++++ .../RequestProcessor.scala | 94 +++++++++++++++++++ settings.gradle | 2 + 4 files changed, 147 insertions(+) create mode 100644 instrumentation-security/http4s-ember-server-2.13_0.23/build.gradle create mode 100644 instrumentation-security/http4s-ember-server-2.13_0.23/src/main/java/org/http4s/ember/server/EmberServerBuilder_Instrumentation.java create mode 100644 instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala diff --git a/instrumentation-security/http4s-ember-server-2.13_0.23/build.gradle b/instrumentation-security/http4s-ember-server-2.13_0.23/build.gradle new file mode 100644 index 000000000..14ca1a788 --- /dev/null +++ b/instrumentation-security/http4s-ember-server-2.13_0.23/build.gradle @@ -0,0 +1,27 @@ +apply plugin: 'scala' + +isScalaProjectEnabled(project, "scala-2.13") + +dependencies { + implementation(project(":newrelic-security-api")) + implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}") + implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}") + implementation("org.scala-lang:scala-library:2.13.3") + implementation('org.http4s:http4s-ember-server_2.13:0.23.12') + implementation("org.typelevel:cats-effect_2.13:3.3.12"){transitive = false} + testImplementation("org.http4s:http4s-dsl_2.12:0.23.12") +} + +jar { + manifest { + attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.http4s-ember-server-2.13_0.23', 'Priority': '10' + } +} + +verifyInstrumentation { + passes 'org.http4s:http4s-ember-server_2.13:[0.23.0,0.24.0)' + excludeRegex '.*(RC|M)[0-9]*' +} + +sourceSets.main.scala.srcDirs = ['src/main/scala', 'src/main/java'] +sourceSets.main.java.srcDirs = [] diff --git a/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/java/org/http4s/ember/server/EmberServerBuilder_Instrumentation.java b/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/java/org/http4s/ember/server/EmberServerBuilder_Instrumentation.java new file mode 100644 index 000000000..84c89eeb5 --- /dev/null +++ b/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/java/org/http4s/ember/server/EmberServerBuilder_Instrumentation.java @@ -0,0 +1,24 @@ +package org.http4s.ember.server; + +import cats.data.Kleisli; +import cats.effect.kernel.Async; +import com.newrelic.agent.security.http4s.ember.server.RequestProcessor$; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import org.http4s.Request; +import org.http4s.Response; + +@Weave(originalName = "org.http4s.ember.server.EmberServerBuilder") +public class EmberServerBuilder_Instrumentation { + + public final Async org$http4s$ember$server$EmberServerBuilder$$evidence$1 = Weaver.callOriginal(); + + public EmberServerBuilder withHttpApp(Kleisli, Response> httpApp) { + try { + httpApp = RequestProcessor$.MODULE$.processHttpApp(httpApp, this.org$http4s$ember$server$EmberServerBuilder$$evidence$1); + } catch (Exception e) { + } + return Weaver.callOriginal(); + } +} + diff --git a/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala b/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala new file mode 100644 index 000000000..32dd87bce --- /dev/null +++ b/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala @@ -0,0 +1,94 @@ +package com.newrelic.agent.security.http4s.ember.server + +import cats.data.Kleisli +import cats.effect.Sync +import cats.implicits._ +import com.comcast.ip4s.Port +import com.newrelic.api.agent.{NewRelic} +import com.newrelic.api.agent.security.NewRelicSecurity +import com.newrelic.api.agent.security.instrumentation.helpers.{GenericHelper, ServletHelper} +import com.newrelic.api.agent.security.schema.{AgentMetaData, HttpRequest, SecurityMetaData, StringUtils} +import com.newrelic.api.agent.security.utils.logging.LogLevel +import org.http4s.{Request, Response} + +import java.util + + +object RequestProcessor{ + private val METHOD_WITH_HTTP_APP = "withHttpApp" + private val NR_SEC_CUSTOM_ATTRIB_NAME = "HTTP4S_EMBER_SERVER_LOCK-" + private val HTTP_4S_EMBER_SERVER_2_13_0_23 = "HTTP4S-EMBER-SERVER-2.13_0.23" + private val X_FORWARDED_FOR = "x-forwarded-for" + private val QUESTION_MARK = "?" + + def processHttpApp[F[_] : Sync](httpApp: Kleisli[F, Request[F], Response[F]]): Kleisli[F, Request[F], Response[F]] = { + Kleisli { req: Request[F] => nrRequestResponse(req, httpApp) } + } + + 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 { + _ <- processReq(request) + resp <- httpApp(request) + } yield resp + ) + result + } + + def processReq[F[_]: Sync](request: Request[F]): F[Unit] = construct { + println("CSEC : Instrumentation active (false in case of NoOp-Transaction) : " + NewRelicSecurity.isHookProcessingActive) + println("CSEC : Instance of NR Transaction : " + NewRelic.getAgent.getTransaction.getClass) + preProcessHttpRequest(true, request) + } + private def construct[F[_]: Sync, T](t: => T): F[T] = Sync[F].delay(t) + + private def preProcessHttpRequest[F[_]: Sync](isServletLockAcquired: Boolean, request: Request[F]): Unit = { + if (!(isServletLockAcquired)) { + return + } + try { + if (!NewRelicSecurity.isHookProcessingActive) { + return + } + val securityMetaData: SecurityMetaData = NewRelicSecurity.getAgent.getSecurityMetaData + val securityRequest: HttpRequest = securityMetaData.getRequest + if (securityRequest.isRequestParsed) { + return + } + val securityAgentMetaData: AgentMetaData = securityMetaData.getMetaData + securityRequest.setMethod(request.method.name) + securityRequest.setClientPort(request.remotePort.get.toString) + //TODO Client IP extraction is pending + + securityRequest.setServerPort(((request.serverPort).get.asInstanceOf[Port]).value) + // TODO Process HTTP Request Headers + securityMetaData.setTracingHeaderValue(getTraceHeader(securityRequest.getHeaders)) + securityRequest.setProtocol(getProtocol(request.isSecure.get)) + securityRequest.setUrl(request.uri.toString) + + // TODO extract request body + // TODO user class detection + + securityRequest.setRequestParsed(true) + } catch { + case ignored => ignored.printStackTrace() + NewRelicSecurity.getAgent.log(LogLevel.WARNING, String.format(GenericHelper.ERROR_GENERATING_HTTP_REQUEST, HTTP_4S_EMBER_SERVER_2_13_0_23, ignored.getMessage), ignored, this.getClass.getName) + } finally { + } + } + + private def getTraceHeader(headers: util.Map[String, String]): String = { + var data: String = 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 getProtocol(isSecure: Boolean): String = { + if (isSecure) "https" + else "http" + } +} diff --git a/settings.gradle b/settings.gradle index 6036165a9..c7f0c1991 100644 --- a/settings.gradle +++ b/settings.gradle @@ -189,3 +189,5 @@ include 'instrumentation:servlet-3.0' include 'instrumentation:spray-http-1.3.1' include 'instrumentation:spray-client' include 'instrumentation:spray-can-1.3.1' +include 'instrumentation:http4s-ember-server-2.13_0.23' + From e256f8c23caa76c723b08676f17582364b89c037 Mon Sep 17 00:00:00 2001 From: Oren Ben-Meir Date: Wed, 10 Jul 2024 14:27:01 -0400 Subject: [PATCH 02/14] http4s instrumentation module priority set to -1 --- .../http4s-ember-server-2.13_0.23/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation-security/http4s-ember-server-2.13_0.23/build.gradle b/instrumentation-security/http4s-ember-server-2.13_0.23/build.gradle index 14ca1a788..704c8d2eb 100644 --- a/instrumentation-security/http4s-ember-server-2.13_0.23/build.gradle +++ b/instrumentation-security/http4s-ember-server-2.13_0.23/build.gradle @@ -14,7 +14,7 @@ dependencies { jar { manifest { - attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.http4s-ember-server-2.13_0.23', 'Priority': '10' + attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.http4s-ember-server-2.13_0.23', 'Priority': '-1' } } From 45f2e4bb10978cd35a0d9ec9d7212a46ebd3e08c Mon Sep 17 00:00:00 2001 From: idawda Date: Thu, 1 Aug 2024 17:33:50 +0530 Subject: [PATCH 03/14] Instrumentation support for http4s-ember server --- .../build.gradle | 27 +++ .../EmberServerBuilder_Instrumentation.java | 20 +++ .../EmberUtils.java | 67 +++++++ .../RequestProcessor.scala | 158 ++++++++++++++++ .../EmberServerBuilder_Instrumentation.java | 5 +- .../EmberUtils.java | 67 +++++++ .../RequestProcessor.scala | 168 ++++++++++++------ settings.gradle | 2 + 8 files changed, 458 insertions(+), 56 deletions(-) create mode 100644 instrumentation-security/http4s-ember-server-2.12_0.23/build.gradle create mode 100644 instrumentation-security/http4s-ember-server-2.12_0.23/src/main/java/org/http4s/ember/server/EmberServerBuilder_Instrumentation.java create mode 100644 instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/EmberUtils.java create mode 100644 instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala create mode 100644 instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/EmberUtils.java diff --git a/instrumentation-security/http4s-ember-server-2.12_0.23/build.gradle b/instrumentation-security/http4s-ember-server-2.12_0.23/build.gradle new file mode 100644 index 000000000..c2aa21729 --- /dev/null +++ b/instrumentation-security/http4s-ember-server-2.12_0.23/build.gradle @@ -0,0 +1,27 @@ +apply plugin: 'scala' + +isScalaProjectEnabled(project, "scala-2.12") + +dependencies { + implementation(project(":newrelic-security-api")) + implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}") + implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}") + implementation("org.scala-lang:scala-library:2.12.14") + implementation('org.http4s:http4s-ember-client_2.12:0.23.12') + implementation("org.typelevel:cats-effect_2.12:3.3.0") + testImplementation("org.http4s:http4s-dsl_2.12:0.23.12") +} + +jar { + manifest { + attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.http4s-ember-server-2.12_0.23', 'Priority': '-1' + } +} + +verifyInstrumentation { + passes 'org.http4s:http4s-ember-client_2.12:[0.23.0,0.24.0)' + excludeRegex '.*(RC|M)[0-9]*' +} + +sourceSets.main.scala.srcDirs = ['src/main/scala', 'src/main/java'] +sourceSets.main.java.srcDirs = [] diff --git a/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/java/org/http4s/ember/server/EmberServerBuilder_Instrumentation.java b/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/java/org/http4s/ember/server/EmberServerBuilder_Instrumentation.java new file mode 100644 index 000000000..1a7b7e891 --- /dev/null +++ b/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/java/org/http4s/ember/server/EmberServerBuilder_Instrumentation.java @@ -0,0 +1,20 @@ +package org.http4s.ember.server; + +import cats.data.Kleisli; +import cats.effect.kernel.Async; +import com.newrelic.agent.security.http4s.ember.server.RequestProcessor$; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import org.http4s.Request; +import org.http4s.Response; + +@Weave(originalName = "org.http4s.ember.server.EmberServerBuilder") +public class EmberServerBuilder_Instrumentation { + + public final Async org$http4s$ember$server$EmberServerBuilder$$evidence$1 = Weaver.callOriginal(); + + public EmberServerBuilder_Instrumentation withHttpApp(Kleisli, Response> httpApp) { + httpApp = RequestProcessor$.MODULE$.genHttpApp(httpApp, this.org$http4s$ember$server$EmberServerBuilder$$evidence$1); + return Weaver.callOriginal(); + } +} diff --git a/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/EmberUtils.java b/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/EmberUtils.java new file mode 100644 index 000000000..ac19a397b --- /dev/null +++ b/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/EmberUtils.java @@ -0,0 +1,67 @@ +package com.newrelic.agent.security.http4s.ember.server; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; +import com.newrelic.api.agent.security.schema.StringUtils; + +import java.util.Map; + +public class EmberUtils { + + public static String getContentType(Map headers) { + String contentType = StringUtils.EMPTY; + if (headers.containsKey("content-type")){ + contentType = headers.get("content-type"); + } + return contentType; + } + + public static String getTraceHeader(Map headers) { + String 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()); + } + } + return data; + } + + public static String getProtocol(boolean isSecure) { + if (isSecure) { + return "https"; + } + return "http"; + } + + + private static boolean isLockAcquired() { + try { + return NewRelicSecurity.isHookProcessingActive() && + Boolean.TRUE.equals(NewRelicSecurity.getAgent().getSecurityMetaData().getCustomAttribute(getNrSecCustomAttribName(), Boolean.class)); + } catch (Throwable ignored) {} + return false; + } + + public static boolean acquireLockIfPossible() { + try { + if (NewRelicSecurity.isHookProcessingActive() && !isLockAcquired()) { + NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(getNrSecCustomAttribName(), true); + return true; + } + } catch (Throwable ignored){} + return false; + } + + public static void releaseLock() { + try { + if(NewRelicSecurity.isHookProcessingActive()) { + NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(getNrSecCustomAttribName(), null); + } + } catch (Throwable ignored){} + } + + private static String getNrSecCustomAttribName() { + return "HTTP4S-EMBER-REQUEST_LOCK" + Thread.currentThread().getId(); + } +} diff --git a/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala b/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala new file mode 100644 index 000000000..6b9bfbd9c --- /dev/null +++ b/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala @@ -0,0 +1,158 @@ +package com.newrelic.agent.security.http4s.ember.server + +import cats.data.Kleisli +import cats.effect.Sync +import cats.implicits._ +import com.comcast.ip4s.Port +import com.newrelic.api.agent.security.NewRelicSecurity +import com.newrelic.api.agent.security.instrumentation.helpers.{GenericHelper, ICsecApiConstants, ServletHelper} +import com.newrelic.api.agent.security.schema._ +import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityException +import com.newrelic.api.agent.security.schema.operation.RXSSOperation +import com.newrelic.api.agent.security.schema.policy.AgentPolicy +import com.newrelic.api.agent.security.utils.logging.LogLevel +import org.http4s.{Headers, Request, Response} + +import java.util + + +object RequestProcessor { + + private val METHOD_WITH_HTTP_APP = "withHttpApp" + private val HTTP_4S_EMBER_SERVER_2_12_0_23 = "HTTP4S-EMBER-SERVER-2.12_0.23" + private val X_FORWARDED_FOR = "x-forwarded-for" + + def genHttpApp[F[_] : Sync](httpApp: Kleisli[F, Request[F], Response[F]]): Kleisli[F, Request[F], Response[F]] = { + Kleisli { req: Request[F] => nrRequestResponse(req, httpApp) } + } + + private def nrRequestResponse[F[_] : Sync](request: Request[F], httpApp: Kleisli[F, Request[F], Response[F]]): F[Response[F]] = { + val result = construct((): Unit) + .redeemWith(_ => httpApp(request), + _ => for { + _ <- preprocessHttpRequest(request) + resp <- httpApp(request) + _ <- postProcessSecurityHook(resp) + } yield resp + ) + result + } + + private def preprocessHttpRequest[F[_]: Sync](request: Request[F]): F[Unit] = construct { + val isLockAcquired = EmberUtils.acquireLockIfPossible() + 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(EmberUtils.getProtocol(request.isSecure.get)) + 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(EmberUtils.getTraceHeader(securityRequest.getHeaders)) + securityRequest.setContentType(EmberUtils.getContentType(securityRequest.getHeaders)) + + // TODO extract request body & user class detection + + val trace: Array[StackTraceElement] = Thread.currentThread.getStackTrace + securityMetaData.getMetaData.setServiceTrace(util.Arrays.copyOfRange(trace, 1, trace.length)) + securityRequest.setRequestParsed(true) + } + + } catch { + case e: Throwable => NewRelicSecurity.getAgent.log(LogLevel.WARNING, String.format(GenericHelper.ERROR_GENERATING_HTTP_REQUEST, HTTP_4S_EMBER_SERVER_2_12_0_23, e.getMessage), e, this.getClass.getName) + } finally { + if (isLockAcquired) { + EmberUtils.releaseLock() + } + } + } + + private def postProcessSecurityHook[F[_]: Sync](response: Response[F]): F[Unit] = construct { + try { + if (NewRelicSecurity.isHookProcessingActive) { + val securityResponse = NewRelicSecurity.getAgent.getSecurityMetaData.getResponse + securityResponse.setResponseCode(response.status.code) + processResponseHeaders(response.headers, securityResponse) + securityResponse.setResponseContentType(EmberUtils.getContentType(securityResponse.getHeaders)) + + // TODO extract response body + + ServletHelper.executeBeforeExitingTransaction() + if (!ServletHelper.isResponseContentTypeExcluded(NewRelicSecurity.getAgent.getSecurityMetaData.getResponse.getResponseContentType)) { + val rxssOperation = new RXSSOperation(NewRelicSecurity.getAgent.getSecurityMetaData.getRequest, NewRelicSecurity.getAgent.getSecurityMetaData.getResponse, this.getClass.getName, METHOD_WITH_HTTP_APP) + NewRelicSecurity.getAgent.registerOperation(rxssOperation) + } + } + } catch { + case e: Throwable => + if (e.isInstanceOf[NewRelicSecurityException]) { + NewRelicSecurity.getAgent.log(LogLevel.WARNING, String.format(GenericHelper.SECURITY_EXCEPTION_MESSAGE, HTTP_4S_EMBER_SERVER_2_12_0_23, e.getMessage), e, this.getClass.getName) + throw e + } + NewRelicSecurity.getAgent.log(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP_4S_EMBER_SERVER_2_12_0_23, e.getMessage), e, this.getClass.getName) + NewRelicSecurity.getAgent.reportIncident(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP_4S_EMBER_SERVER_2_12_0_23, e.getMessage), e, this.getClass.getName) + } + } + + private def processRequestHeaders(headers: Headers, securityRequest: HttpRequest): Unit = { + headers.foreach(header => { + var takeNextValue = false + var headerKey: String = StringUtils.EMPTY + if (header.name != null && header.name.nonEmpty) { + headerKey = header.name.toString + } + val headerValue: String = header.value + + val agentPolicy: AgentPolicy = NewRelicSecurity.getAgent.getCurrentPolicy + val agentMetaData: AgentMetaData = NewRelicSecurity.getAgent.getSecurityMetaData.getMetaData + if (agentPolicy != null + && agentPolicy.getProtectionMode.getEnabled() + && agentPolicy.getProtectionMode.getIpBlocking.getEnabled() + && agentPolicy.getProtectionMode.getIpBlocking.getIpDetectViaXFF() + && X_FORWARDED_FOR.equals(headerKey)) { + takeNextValue = true + } else if (ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID == headerKey) { + // TODO: May think of removing this intermediate obj and directly create K2 Identifier. + NewRelicSecurity.getAgent.getSecurityMetaData.setFuzzRequestIdentifier(ServletHelper.parseFuzzRequestIdentifierHeader(headerValue)) + } + if (GenericHelper.CSEC_PARENT_ID == headerKey) { + NewRelicSecurity.getAgent.getSecurityMetaData.addCustomAttribute(GenericHelper.CSEC_PARENT_ID, headerValue) + } + else if (ICsecApiConstants.NR_CSEC_JAVA_HEAD_REQUEST == headerKey) { + NewRelicSecurity.getAgent.getSecurityMetaData.addCustomAttribute(ICsecApiConstants.NR_CSEC_JAVA_HEAD_REQUEST, true) + } + + if (headerValue != null && headerValue.trim.nonEmpty) { + if (takeNextValue) { + agentMetaData.setClientDetectedFromXFF(true) + securityRequest.setClientIP(headerValue) + agentMetaData.getIps.add(securityRequest.getClientIP) + securityRequest.setClientPort(StringUtils.EMPTY) + takeNextValue = false + } + } + securityRequest.getHeaders.put(headerKey.toLowerCase, headerValue) + }) + } + + private def processResponseHeaders(headers: Headers, securityResp: HttpResponse): Unit = { + headers.foreach(header => { + if (header.name != null && header.name.nonEmpty) { + securityResp.getHeaders.put(header.name.toString.toLowerCase, header.value) + } + }) + } + + private def construct[F[_] : Sync, T](t: => T): F[T] = Sync[F].delay(t) +} diff --git a/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/java/org/http4s/ember/server/EmberServerBuilder_Instrumentation.java b/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/java/org/http4s/ember/server/EmberServerBuilder_Instrumentation.java index 84c89eeb5..d7beb29c2 100644 --- a/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/java/org/http4s/ember/server/EmberServerBuilder_Instrumentation.java +++ b/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/java/org/http4s/ember/server/EmberServerBuilder_Instrumentation.java @@ -14,10 +14,7 @@ public class EmberServerBuilder_Instrumentation { public final Async org$http4s$ember$server$EmberServerBuilder$$evidence$1 = Weaver.callOriginal(); public EmberServerBuilder withHttpApp(Kleisli, Response> httpApp) { - try { - httpApp = RequestProcessor$.MODULE$.processHttpApp(httpApp, this.org$http4s$ember$server$EmberServerBuilder$$evidence$1); - } catch (Exception e) { - } + httpApp = RequestProcessor$.MODULE$.genHttpApp(httpApp, this.org$http4s$ember$server$EmberServerBuilder$$evidence$1); return Weaver.callOriginal(); } } diff --git a/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/EmberUtils.java b/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/EmberUtils.java new file mode 100644 index 000000000..ac19a397b --- /dev/null +++ b/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/EmberUtils.java @@ -0,0 +1,67 @@ +package com.newrelic.agent.security.http4s.ember.server; + +import com.newrelic.api.agent.security.NewRelicSecurity; +import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; +import com.newrelic.api.agent.security.schema.StringUtils; + +import java.util.Map; + +public class EmberUtils { + + public static String getContentType(Map headers) { + String contentType = StringUtils.EMPTY; + if (headers.containsKey("content-type")){ + contentType = headers.get("content-type"); + } + return contentType; + } + + public static String getTraceHeader(Map headers) { + String 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()); + } + } + return data; + } + + public static String getProtocol(boolean isSecure) { + if (isSecure) { + return "https"; + } + return "http"; + } + + + private static boolean isLockAcquired() { + try { + return NewRelicSecurity.isHookProcessingActive() && + Boolean.TRUE.equals(NewRelicSecurity.getAgent().getSecurityMetaData().getCustomAttribute(getNrSecCustomAttribName(), Boolean.class)); + } catch (Throwable ignored) {} + return false; + } + + public static boolean acquireLockIfPossible() { + try { + if (NewRelicSecurity.isHookProcessingActive() && !isLockAcquired()) { + NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(getNrSecCustomAttribName(), true); + return true; + } + } catch (Throwable ignored){} + return false; + } + + public static void releaseLock() { + try { + if(NewRelicSecurity.isHookProcessingActive()) { + NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(getNrSecCustomAttribName(), null); + } + } catch (Throwable ignored){} + } + + private static String getNrSecCustomAttribName() { + return "HTTP4S-EMBER-REQUEST_LOCK" + Thread.currentThread().getId(); + } +} diff --git a/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala b/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala index 32dd87bce..88de7997b 100644 --- a/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala +++ b/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala @@ -4,91 +4,155 @@ import cats.data.Kleisli import cats.effect.Sync import cats.implicits._ import com.comcast.ip4s.Port -import com.newrelic.api.agent.{NewRelic} import com.newrelic.api.agent.security.NewRelicSecurity -import com.newrelic.api.agent.security.instrumentation.helpers.{GenericHelper, ServletHelper} -import com.newrelic.api.agent.security.schema.{AgentMetaData, HttpRequest, SecurityMetaData, StringUtils} +import com.newrelic.api.agent.security.instrumentation.helpers.{GenericHelper, ICsecApiConstants, ServletHelper} +import com.newrelic.api.agent.security.schema._ +import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityException +import com.newrelic.api.agent.security.schema.operation.RXSSOperation +import com.newrelic.api.agent.security.schema.policy.AgentPolicy import com.newrelic.api.agent.security.utils.logging.LogLevel -import org.http4s.{Request, Response} +import org.http4s.{Headers, Request, Response} import java.util -object RequestProcessor{ +object RequestProcessor { + private val METHOD_WITH_HTTP_APP = "withHttpApp" - private val NR_SEC_CUSTOM_ATTRIB_NAME = "HTTP4S_EMBER_SERVER_LOCK-" private val HTTP_4S_EMBER_SERVER_2_13_0_23 = "HTTP4S-EMBER-SERVER-2.13_0.23" private val X_FORWARDED_FOR = "x-forwarded-for" - private val QUESTION_MARK = "?" - def processHttpApp[F[_] : Sync](httpApp: Kleisli[F, Request[F], Response[F]]): Kleisli[F, Request[F], Response[F]] = { + def genHttpApp[F[_] : Sync](httpApp: Kleisli[F, Request[F], Response[F]]): Kleisli[F, Request[F], Response[F]] = { Kleisli { req: Request[F] => nrRequestResponse(req, httpApp) } } - def nrRequestResponse[F[_] : Sync](request: Request[F], httpApp: Kleisli[F, Request[F], Response[F]]): F[Response[F]] = { + 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 { - _ <- processReq(request) + _ <- preprocessHttpRequest(request) resp <- httpApp(request) + _ <- postProcessSecurityHook(resp) } yield resp ) result } - def processReq[F[_]: Sync](request: Request[F]): F[Unit] = construct { - println("CSEC : Instrumentation active (false in case of NoOp-Transaction) : " + NewRelicSecurity.isHookProcessingActive) - println("CSEC : Instance of NR Transaction : " + NewRelic.getAgent.getTransaction.getClass) - preProcessHttpRequest(true, request) - } - private def construct[F[_]: Sync, T](t: => T): F[T] = Sync[F].delay(t) - - private def preProcessHttpRequest[F[_]: Sync](isServletLockAcquired: Boolean, request: Request[F]): Unit = { - if (!(isServletLockAcquired)) { - return - } + private def preprocessHttpRequest[F[_]: Sync](request: Request[F]): F[Unit] = construct { + val isLockAcquired = EmberUtils.acquireLockIfPossible() try { - if (!NewRelicSecurity.isHookProcessingActive) { - return - } - val securityMetaData: SecurityMetaData = NewRelicSecurity.getAgent.getSecurityMetaData - val securityRequest: HttpRequest = securityMetaData.getRequest - if (securityRequest.isRequestParsed) { - return - } - val securityAgentMetaData: AgentMetaData = securityMetaData.getMetaData - securityRequest.setMethod(request.method.name) - securityRequest.setClientPort(request.remotePort.get.toString) - //TODO Client IP extraction is pending + 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.setServerPort(((request.serverPort).get.asInstanceOf[Port]).value) - // TODO Process HTTP Request Headers - securityMetaData.setTracingHeaderValue(getTraceHeader(securityRequest.getHeaders)) - securityRequest.setProtocol(getProtocol(request.isSecure.get)) - securityRequest.setUrl(request.uri.toString) + securityRequest.setMethod(request.method.name) + securityRequest.setServerPort((request.serverPort).get.asInstanceOf[Port].value) + securityRequest.setClientIP(request.remoteAddr.get.toString) + securityRequest.setProtocol(EmberUtils.getProtocol(request.isSecure.get)) + securityRequest.setUrl(request.uri.toString) - // TODO extract request body - // TODO user class detection + 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(EmberUtils.getTraceHeader(securityRequest.getHeaders)) + securityRequest.setContentType(EmberUtils.getContentType(securityRequest.getHeaders)) + + // TODO extract request body & user class detection + + val trace: Array[StackTraceElement] = Thread.currentThread.getStackTrace + securityMetaData.getMetaData.setServiceTrace(util.Arrays.copyOfRange(trace, 1, trace.length)) + securityRequest.setRequestParsed(true) + } - securityRequest.setRequestParsed(true) } catch { - case ignored => ignored.printStackTrace() - NewRelicSecurity.getAgent.log(LogLevel.WARNING, String.format(GenericHelper.ERROR_GENERATING_HTTP_REQUEST, HTTP_4S_EMBER_SERVER_2_13_0_23, ignored.getMessage), ignored, this.getClass.getName) + case e: Throwable => NewRelicSecurity.getAgent.log(LogLevel.WARNING, String.format(GenericHelper.ERROR_GENERATING_HTTP_REQUEST, HTTP_4S_EMBER_SERVER_2_13_0_23, e.getMessage), e, this.getClass.getName) } finally { + if (isLockAcquired) { + EmberUtils.releaseLock() + } } } - private def getTraceHeader(headers: util.Map[String, String]): String = { - var data: String = 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) + private def postProcessSecurityHook[F[_]: Sync](response: Response[F]): F[Unit] = construct { + try { + if (NewRelicSecurity.isHookProcessingActive) { + val securityResponse = NewRelicSecurity.getAgent.getSecurityMetaData.getResponse + securityResponse.setResponseCode(response.status.code) + processResponseHeaders(response.headers, securityResponse) + securityResponse.setResponseContentType(EmberUtils.getContentType(securityResponse.getHeaders)) + + // TODO extract response body + + ServletHelper.executeBeforeExitingTransaction() + if (!ServletHelper.isResponseContentTypeExcluded(NewRelicSecurity.getAgent.getSecurityMetaData.getResponse.getResponseContentType)) { + val rxssOperation = new RXSSOperation(NewRelicSecurity.getAgent.getSecurityMetaData.getRequest, NewRelicSecurity.getAgent.getSecurityMetaData.getResponse, this.getClass.getName, METHOD_WITH_HTTP_APP) + NewRelicSecurity.getAgent.registerOperation(rxssOperation) + } + } + } catch { + case e: Throwable => + if (e.isInstanceOf[NewRelicSecurityException]) { + NewRelicSecurity.getAgent.log(LogLevel.WARNING, String.format(GenericHelper.SECURITY_EXCEPTION_MESSAGE, HTTP_4S_EMBER_SERVER_2_13_0_23, e.getMessage), e, this.getClass.getName) + throw e + } + NewRelicSecurity.getAgent.log(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP_4S_EMBER_SERVER_2_13_0_23, e.getMessage), e, this.getClass.getName) + NewRelicSecurity.getAgent.reportIncident(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP_4S_EMBER_SERVER_2_13_0_23, e.getMessage), e, this.getClass.getName) } - data } - private def getProtocol(isSecure: Boolean): String = { - if (isSecure) "https" - else "http" + private def processRequestHeaders(headers: Headers, securityRequest: HttpRequest): Unit = { + headers.foreach(header => { + var takeNextValue = false + var headerKey: String = StringUtils.EMPTY + if (header.name != null && header.name.nonEmpty) { + headerKey = header.name.toString + } + val headerValue: String = header.value + + val agentPolicy: AgentPolicy = NewRelicSecurity.getAgent.getCurrentPolicy + val agentMetaData: AgentMetaData = NewRelicSecurity.getAgent.getSecurityMetaData.getMetaData + if (agentPolicy != null + && agentPolicy.getProtectionMode.getEnabled() + && agentPolicy.getProtectionMode.getIpBlocking.getEnabled() + && agentPolicy.getProtectionMode.getIpBlocking.getIpDetectViaXFF() + && X_FORWARDED_FOR.equals(headerKey)) { + takeNextValue = true + } else if (ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID == headerKey) { + // TODO: May think of removing this intermediate obj and directly create K2 Identifier. + NewRelicSecurity.getAgent.getSecurityMetaData.setFuzzRequestIdentifier(ServletHelper.parseFuzzRequestIdentifierHeader(headerValue)) + } + if (GenericHelper.CSEC_PARENT_ID == headerKey) { + NewRelicSecurity.getAgent.getSecurityMetaData.addCustomAttribute(GenericHelper.CSEC_PARENT_ID, headerValue) + } + else if (ICsecApiConstants.NR_CSEC_JAVA_HEAD_REQUEST == headerKey) { + NewRelicSecurity.getAgent.getSecurityMetaData.addCustomAttribute(ICsecApiConstants.NR_CSEC_JAVA_HEAD_REQUEST, true) + } + + if (headerValue != null && headerValue.trim.nonEmpty) { + if (takeNextValue) { + agentMetaData.setClientDetectedFromXFF(true) + securityRequest.setClientIP(headerValue) + agentMetaData.getIps.add(securityRequest.getClientIP) + securityRequest.setClientPort(StringUtils.EMPTY) + takeNextValue = false + } + } + securityRequest.getHeaders.put(headerKey.toLowerCase, headerValue) + }) + } + + private def processResponseHeaders(headers: Headers, securityResp: HttpResponse): Unit = { + headers.foreach(header => { + if (header.name != null && header.name.nonEmpty) { + securityResp.getHeaders.put(header.name.toString.toLowerCase, header.value) + } + }) } + + private def construct[F[_] : Sync, T](t: => T): F[T] = Sync[F].delay(t) } diff --git a/settings.gradle b/settings.gradle index ca13695b0..15c187c6e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -209,3 +209,5 @@ include 'instrumentation:weblogic-12.2' include 'instrumentation:jedis-4.0.0' include 'instrumentation:jedis-3.0.0' include 'instrumentation:jedis-2.7.1_2.7.2' +include 'instrumentation:http4s-ember-server-2.12_0.23' +include 'instrumentation:http4s-ember-server-2.13_0.23' \ No newline at end of file From 29fffb3518d1d38954828964911250d2686b6f81 Mon Sep 17 00:00:00 2001 From: idawda Date: Thu, 10 Oct 2024 14:26:03 +0530 Subject: [PATCH 04/14] NR-307675: Instrument outgoing HTTP Request in HTTP4s-Ember-Client --- .../build.gradle | 27 +++++ .../EmberClientBuilder_Instrumentation.java | 20 ++++ .../NewrelicSecurityClientMiddleware.scala | 99 +++++++++++++++++++ .../build.gradle | 27 +++++ .../EmberClientBuilder_Instrumentation.java | 19 ++++ .../NewrelicSecurityClientMiddleware.scala | 98 ++++++++++++++++++ settings.gradle | 4 +- 7 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 instrumentation-security/http4s-ember-client-2.12_0.23/build.gradle create mode 100644 instrumentation-security/http4s-ember-client-2.12_0.23/src/main/java/org/http4s/EmberClientBuilder_Instrumentation.java create mode 100644 instrumentation-security/http4s-ember-client-2.12_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/NewrelicSecurityClientMiddleware.scala create mode 100644 instrumentation-security/http4s-ember-client-2.13_0.23/build.gradle create mode 100644 instrumentation-security/http4s-ember-client-2.13_0.23/src/main/java/org/http4s/EmberClientBuilder_Instrumentation.java create mode 100644 instrumentation-security/http4s-ember-client-2.13_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/NewrelicSecurityClientMiddleware.scala diff --git a/instrumentation-security/http4s-ember-client-2.12_0.23/build.gradle b/instrumentation-security/http4s-ember-client-2.12_0.23/build.gradle new file mode 100644 index 000000000..1bb37776e --- /dev/null +++ b/instrumentation-security/http4s-ember-client-2.12_0.23/build.gradle @@ -0,0 +1,27 @@ +apply plugin: 'scala' + +isScalaProjectEnabled(project, "scala-2.12") + +dependencies { + implementation(project(":newrelic-security-api")) + implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}") + implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}") + implementation("org.scala-lang:scala-library:2.12.14") + implementation('org.http4s:http4s-ember-client_2.12:0.23.12') + implementation("org.typelevel:cats-effect_2.12:3.3.0") + testImplementation("org.http4s:http4s-dsl_2.12:0.23.12") + +} + +jar { + manifest { + attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.http4s-ember-client-2.12_0.23' + } +} +verifyInstrumentation { + passes 'org.http4s:http4s-ember-client_2.12:[0.23.0,0.24.0)' + excludeRegex '.*(RC|M)[0-9]*' +} + +sourceSets.main.scala.srcDirs = ['src/main/scala', 'src/main/java'] +sourceSets.main.java.srcDirs = [] diff --git a/instrumentation-security/http4s-ember-client-2.12_0.23/src/main/java/org/http4s/EmberClientBuilder_Instrumentation.java b/instrumentation-security/http4s-ember-client-2.12_0.23/src/main/java/org/http4s/EmberClientBuilder_Instrumentation.java new file mode 100644 index 000000000..5e56dfaa3 --- /dev/null +++ b/instrumentation-security/http4s-ember-client-2.12_0.23/src/main/java/org/http4s/EmberClientBuilder_Instrumentation.java @@ -0,0 +1,20 @@ +package org.http4s; + +import cats.effect.kernel.Async; +import cats.effect.kernel.Resource; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import com.newrelic.agent.security.instrumentation.http4s.ember.NewrelicSecurityClientMiddleware$; +import org.http4s.client.Client; + +@Weave(type = MatchType.ExactClass, originalName = "org.http4s.ember.client.EmberClientBuilder") +public abstract class EmberClientBuilder_Instrumentation { + + private final Async evidence$1 = Weaver.callOriginal(); + + public Resource> build() { + Resource> delegateResource = Weaver.callOriginal(); + return NewrelicSecurityClientMiddleware$.MODULE$.resource(delegateResource, evidence$1); + } +} diff --git a/instrumentation-security/http4s-ember-client-2.12_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/NewrelicSecurityClientMiddleware.scala b/instrumentation-security/http4s-ember-client-2.12_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/NewrelicSecurityClientMiddleware.scala new file mode 100644 index 000000000..5a960a842 --- /dev/null +++ b/instrumentation-security/http4s-ember-client-2.12_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/NewrelicSecurityClientMiddleware.scala @@ -0,0 +1,99 @@ +package com.newrelic.agent.security.instrumentation.http4s.ember + +import cats.effect.kernel.Async +import cats.effect.{Resource, Sync} +import com.newrelic.api.agent.security.NewRelicSecurity +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper +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, VulnerabilityCaseType} +import com.newrelic.api.agent.security.utils.logging.LogLevel +import org.http4s.Request +import org.http4s.client.Client + +import java.net.URI + +object NewrelicSecurityClientMiddleware { + private final val nrSecCustomAttrName: String = "HTTP4S-EMBER-CLIENT-OUTBOUND" + private final val HTTP4S_EMBER_CLIENT: String = "HTTP4S-EMBER-CLIENT-2.12_0.23" + + private def construct[F[_] : Sync, T](t: T): F[T] = Sync[F].delay(t) + + private def clientResource[F[_] : Async](client: Client[F]): Client[F] = + Client { req: Request[F] => + for { + // pre-process hook + operation <- Resource.eval( + construct { + val isLockAcquired = GenericHelper.acquireLockIfPossible(VulnerabilityCaseType.HTTP_REQUEST, nrSecCustomAttrName) + var operation: AbstractOperation = null + if (isLockAcquired) { + operation = preprocessSecurityHook(req) + } + operation + }) + + // TODO add Security Headers + + // original call + response <- client.run(req) + + // post process and register exit event + newRes <- Resource.eval(construct{ + val isLockAcquired = GenericHelper.isLockAcquired(nrSecCustomAttrName); + if (isLockAcquired) { + GenericHelper.releaseLock(nrSecCustomAttrName) + } + registerExitOperation(isLockAcquired, operation) + response + }) + + } yield newRes + } + + def resource[F[_] : Async](delegate: Resource[F, Client[F]]): Resource[F, Client[F]] = { + val res: Resource[F, Client[F]] = delegate.map(c =>clientResource(c)) + res + } + + + private def preprocessSecurityHook[F[_] : Async](httpRequest: Request[F]): AbstractOperation = { + try { + val securityMetaData = NewRelicSecurity.getAgent.getSecurityMetaData + if (!NewRelicSecurity.isHookProcessingActive || securityMetaData.getRequest.isEmpty) return null + // Generate required URL + var methodURI: URI = null + var uri: String = null + try { + methodURI = new URI(httpRequest.uri.toString) + uri = methodURI.toString + if (methodURI == null) return null + } catch { + case ignored: Exception => + NewRelicSecurity.getAgent.log(LogLevel.WARNING, String.format(GenericHelper.URI_EXCEPTION_MESSAGE, HTTP4S_EMBER_CLIENT, ignored.getMessage), ignored, this.getClass.getName) + return null + } + return new SSRFOperation(uri, this.getClass.getName, "run") + } catch { + case e: Throwable => + if (e.isInstanceOf[NewRelicSecurityException]) { + NewRelicSecurity.getAgent.log(LogLevel.WARNING, String.format(GenericHelper.SECURITY_EXCEPTION_MESSAGE, HTTP4S_EMBER_CLIENT, e.getMessage), e, this.getClass.getName) + throw e + } + NewRelicSecurity.getAgent.log(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP4S_EMBER_CLIENT, e.getMessage), e, this.getClass.getName) + NewRelicSecurity.getAgent.reportIncident(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP4S_EMBER_CLIENT, e.getMessage), e, this.getClass.getName) + } + null + } + + private def registerExitOperation(isProcessingAllowed: Boolean, operation: AbstractOperation): Unit = { + try { + if (operation == null || !isProcessingAllowed || !NewRelicSecurity.isHookProcessingActive || NewRelicSecurity.getAgent.getSecurityMetaData.getRequest.isEmpty) return + NewRelicSecurity.getAgent.registerExitEvent(operation) + } catch { + case e: Throwable => + NewRelicSecurity.getAgent.log(LogLevel.FINEST, String.format(GenericHelper.EXIT_OPERATION_EXCEPTION_MESSAGE, HTTP4S_EMBER_CLIENT, e.getMessage), e, this.getClass.getName) + } + } +} + diff --git a/instrumentation-security/http4s-ember-client-2.13_0.23/build.gradle b/instrumentation-security/http4s-ember-client-2.13_0.23/build.gradle new file mode 100644 index 000000000..88b683f7b --- /dev/null +++ b/instrumentation-security/http4s-ember-client-2.13_0.23/build.gradle @@ -0,0 +1,27 @@ +apply plugin: 'scala' + +isScalaProjectEnabled(project, "scala-2.13") + +dependencies { + implementation(project(":newrelic-security-api")) + implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}") + implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}") + implementation("org.scala-lang:scala-library:2.13.3") + implementation('org.http4s:http4s-ember-client_2.13:0.23.12') + implementation("org.typelevel:cats-effect_2.13:3.3.0") + testImplementation("org.http4s:http4s-dsl_2.13:0.23.12") + +} + +jar { + manifest { + attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.http4s-ember-client-2.13_0.23' + } +} +verifyInstrumentation { + passes 'org.http4s:http4s-ember-client_2.13:[0.23.0,0.24.0)' + excludeRegex '.*(RC|M)[0-9]*' +} + +sourceSets.main.scala.srcDirs = ['src/main/scala', 'src/main/java'] +sourceSets.main.java.srcDirs = [] diff --git a/instrumentation-security/http4s-ember-client-2.13_0.23/src/main/java/org/http4s/EmberClientBuilder_Instrumentation.java b/instrumentation-security/http4s-ember-client-2.13_0.23/src/main/java/org/http4s/EmberClientBuilder_Instrumentation.java new file mode 100644 index 000000000..f702d27f7 --- /dev/null +++ b/instrumentation-security/http4s-ember-client-2.13_0.23/src/main/java/org/http4s/EmberClientBuilder_Instrumentation.java @@ -0,0 +1,19 @@ +package org.http4s; + +import cats.effect.kernel.Async; +import cats.effect.kernel.Resource; +import com.newrelic.agent.security.instrumentation.http4s.ember.NewrelicSecurityClientMiddleware$; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import org.http4s.client.Client; + +@Weave(type = MatchType.ExactClass, originalName = "org.http4s.ember.client.EmberClientBuilder") +public abstract class EmberClientBuilder_Instrumentation { + + private final Async evidence$1 = Weaver.callOriginal(); + public Resource> build() { + Resource> delegateResource = Weaver.callOriginal(); + return NewrelicSecurityClientMiddleware$.MODULE$.resource(delegateResource, evidence$1); + } +} diff --git a/instrumentation-security/http4s-ember-client-2.13_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/NewrelicSecurityClientMiddleware.scala b/instrumentation-security/http4s-ember-client-2.13_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/NewrelicSecurityClientMiddleware.scala new file mode 100644 index 000000000..d112d965a --- /dev/null +++ b/instrumentation-security/http4s-ember-client-2.13_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/NewrelicSecurityClientMiddleware.scala @@ -0,0 +1,98 @@ +package com.newrelic.agent.security.instrumentation.http4s.ember + +import cats.effect.kernel.Async +import cats.effect.{Resource, Sync} +import com.newrelic.api.agent.security.NewRelicSecurity +import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper +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, VulnerabilityCaseType} +import com.newrelic.api.agent.security.utils.logging.LogLevel +import org.http4s.Request +import org.http4s.client.Client + +import java.net.URI + +object NewrelicSecurityClientMiddleware { + + private final val nrSecCustomAttrName: String = "HTTP4S-EMBER-CLIENT-OUTBOUND" + private final val HTTP4S_EMBER_CLIENT: String = "HTTP4S-EMBER-CLIENT-2.13_0.23" + + private def construct[F[_] : Sync, T](t: T): F[T] = Sync[F].delay(t) + + private def clientResource[F[_] : Async](client: Client[F]): Client[F] = + Client { req: Request[F] => + for { + // pre-process hook + operation <- Resource.eval( + construct { + val isLockAcquired = GenericHelper.acquireLockIfPossible(VulnerabilityCaseType.HTTP_REQUEST, nrSecCustomAttrName) + var operation: AbstractOperation = null + if (isLockAcquired) { + operation = preprocessSecurityHook(req) + } + operation + }) + // TODO: add Security Headers + + // original call + response <- client.run(req) + + // post process and register exit event + newRes <- Resource.eval(construct{ + val isLockAcquired = GenericHelper.isLockAcquired(nrSecCustomAttrName); + if (isLockAcquired) { + GenericHelper.releaseLock(nrSecCustomAttrName) + } + registerExitOperation(isLockAcquired, operation) + response + }) + + } yield newRes + } + + def resource[F[_] : Async](delegate: Resource[F, Client[F]]): Resource[F, Client[F]] = { + val res: Resource[F, Client[F]] = delegate.map(c =>clientResource(c)) + res + } + + private def preprocessSecurityHook[F[_] : Async](httpRequest: Request[F]): AbstractOperation = { + try { + val securityMetaData = NewRelicSecurity.getAgent.getSecurityMetaData + if (!NewRelicSecurity.isHookProcessingActive || securityMetaData.getRequest.isEmpty) return null + // Generate required URL + var methodURI: URI = null + var uri: String = null + try { + methodURI = new URI(httpRequest.uri.toString) + uri = methodURI.toString + if (methodURI == null) return null + } catch { + case ignored: Exception => + NewRelicSecurity.getAgent.log(LogLevel.WARNING, String.format(GenericHelper.URI_EXCEPTION_MESSAGE, HTTP4S_EMBER_CLIENT, ignored.getMessage), ignored, this.getClass.getName) + return null + } + return new SSRFOperation(uri, this.getClass.getName, "run") + } catch { + case e: Throwable => + if (e.isInstanceOf[NewRelicSecurityException]) { + NewRelicSecurity.getAgent.log(LogLevel.WARNING, String.format(GenericHelper.SECURITY_EXCEPTION_MESSAGE, HTTP4S_EMBER_CLIENT, e.getMessage), e, this.getClass.getName) + throw e + } + NewRelicSecurity.getAgent.log(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP4S_EMBER_CLIENT, e.getMessage), e, this.getClass.getName) + NewRelicSecurity.getAgent.reportIncident(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP4S_EMBER_CLIENT, e.getMessage), e, this.getClass.getName) + } + null + } + + private def registerExitOperation(isProcessingAllowed: Boolean, operation: AbstractOperation): Unit = { + try { + if (operation == null || !isProcessingAllowed || !NewRelicSecurity.isHookProcessingActive || NewRelicSecurity.getAgent.getSecurityMetaData.getRequest.isEmpty) return + NewRelicSecurity.getAgent.registerExitEvent(operation) + } catch { + case e: Throwable => + NewRelicSecurity.getAgent.log(LogLevel.FINEST, String.format(GenericHelper.EXIT_OPERATION_EXCEPTION_MESSAGE, HTTP4S_EMBER_CLIENT, e.getMessage), e, this.getClass.getName) + } + } +} + diff --git a/settings.gradle b/settings.gradle index a53a1beee..f191e6557 100644 --- a/settings.gradle +++ b/settings.gradle @@ -219,4 +219,6 @@ 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-ember-server-2.12_0.23' -include 'instrumentation:http4s-ember-server-2.13_0.23' \ No newline at end of file +include 'instrumentation:http4s-ember-server-2.13_0.23' +include 'instrumentation:http4s-ember-client-2.13_0.23' +include 'instrumentation:http4s-ember-client-2.12_0.23' \ No newline at end of file From dc61b8da28b7b238c27d8dd0bcfa2cf449c2e5b3 Mon Sep 17 00:00:00 2001 From: idawda Date: Tue, 15 Oct 2024 12:21:20 +0530 Subject: [PATCH 05/14] NR-307676: Manipulate headers of outgoing HTTP Request in HTTP4s-Ember-Client --- .../NewrelicSecurityClientMiddleware.scala | 36 ++++++++++++++++--- .../http4s/ember/OutboundRequest.scala | 19 ++++++++++ .../NewrelicSecurityClientMiddleware.scala | 36 ++++++++++++++++--- .../http4s/ember/OutboundRequest.scala | 20 +++++++++++ 4 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 instrumentation-security/http4s-ember-client-2.12_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/OutboundRequest.scala create mode 100644 instrumentation-security/http4s-ember-client-2.13_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/OutboundRequest.scala diff --git a/instrumentation-security/http4s-ember-client-2.12_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/NewrelicSecurityClientMiddleware.scala b/instrumentation-security/http4s-ember-client-2.12_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/NewrelicSecurityClientMiddleware.scala index 5a960a842..594241802 100644 --- a/instrumentation-security/http4s-ember-client-2.12_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/NewrelicSecurityClientMiddleware.scala +++ b/instrumentation-security/http4s-ember-client-2.12_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/NewrelicSecurityClientMiddleware.scala @@ -3,10 +3,11 @@ package com.newrelic.agent.security.instrumentation.http4s.ember import cats.effect.kernel.Async import cats.effect.{Resource, Sync} import com.newrelic.api.agent.security.NewRelicSecurity -import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper +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, VulnerabilityCaseType} +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 @@ -32,11 +33,11 @@ object NewrelicSecurityClientMiddleware { } operation }) - - // TODO add Security Headers + // add Security Headers + request <- Resource.eval(construct {addSecurityHeaders(req, operation)}) // original call - response <- client.run(req) + response <- client.run(request) // post process and register exit event newRes <- Resource.eval(construct{ @@ -86,6 +87,31 @@ object NewrelicSecurityClientMiddleware { null } + private def addSecurityHeaders[F[_] : Async](request: Request[F], operation: AbstractOperation): Request[F] = { + val outboundRequest = new OutboundRequest(request) + if (operation != null) { + val securityMetaData = NewRelicSecurity.getAgent.getSecurityMetaData + val iastHeader = NewRelicSecurity.getAgent.getSecurityMetaData.getFuzzRequestIdentifier.getRaw + if (iastHeader != null && !iastHeader.trim.isEmpty) { + outboundRequest.setHeader(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID, iastHeader) + } + val csecParentId = securityMetaData.getCustomAttribute(GenericHelper.CSEC_PARENT_ID, classOf[String]) + if (StringUtils.isNotBlank(csecParentId)) { + outboundRequest.setHeader(GenericHelper.CSEC_PARENT_ID, csecParentId) + } + try { + NewRelicSecurity.getAgent.getSecurityMetaData.getMetaData.setFromJumpRequiredInStackTrace(Integer.valueOf(4)) + NewRelicSecurity.getAgent.registerOperation(operation) + } + finally { + if (operation.getApiID != null && !operation.getApiID.trim.isEmpty && operation.getExecutionId != null && !operation.getExecutionId.trim.isEmpty) { + outboundRequest.setHeader(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER, SSRFUtils.generateTracingHeaderValue(securityMetaData.getTracingHeaderValue, operation.getApiID, operation.getExecutionId, NewRelicSecurity.getAgent.getAgentUUID)) + } + } + } + outboundRequest.getRequest + } + private def registerExitOperation(isProcessingAllowed: Boolean, operation: AbstractOperation): Unit = { try { if (operation == null || !isProcessingAllowed || !NewRelicSecurity.isHookProcessingActive || NewRelicSecurity.getAgent.getSecurityMetaData.getRequest.isEmpty) return diff --git a/instrumentation-security/http4s-ember-client-2.12_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/OutboundRequest.scala b/instrumentation-security/http4s-ember-client-2.12_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/OutboundRequest.scala new file mode 100644 index 000000000..ca6b52a95 --- /dev/null +++ b/instrumentation-security/http4s-ember-client-2.12_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/OutboundRequest.scala @@ -0,0 +1,19 @@ +package com.newrelic.agent.security.instrumentation.http4s.ember + +import org.http4s.{Header, Request} +import org.typelevel.ci.CIString + +/** + * Http4s's HttpRequest is immutable so we have to create a copy with the new headers. + */ + +class OutboundRequest[F[_]](request: Request[F]) { + private var req: Request[F] = request + + def setHeader(key: String, value: String): Unit = { + req = req.withHeaders(req.headers.put(Header.Raw.apply(CIString.apply(key), value))) + } + def getRequest: Request[F] = { + req + } +} \ No newline at end of file diff --git a/instrumentation-security/http4s-ember-client-2.13_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/NewrelicSecurityClientMiddleware.scala b/instrumentation-security/http4s-ember-client-2.13_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/NewrelicSecurityClientMiddleware.scala index d112d965a..4abb321f6 100644 --- a/instrumentation-security/http4s-ember-client-2.13_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/NewrelicSecurityClientMiddleware.scala +++ b/instrumentation-security/http4s-ember-client-2.13_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/NewrelicSecurityClientMiddleware.scala @@ -3,10 +3,11 @@ package com.newrelic.agent.security.instrumentation.http4s.ember import cats.effect.kernel.Async import cats.effect.{Resource, Sync} import com.newrelic.api.agent.security.NewRelicSecurity -import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper +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, VulnerabilityCaseType} +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 @@ -33,10 +34,12 @@ object NewrelicSecurityClientMiddleware { } operation }) - // TODO: add Security Headers + + // add Security Headers + request <- Resource.eval(construct {addSecurityHeaders(req, operation)}) // original call - response <- client.run(req) + response <- client.run(request) // post process and register exit event newRes <- Resource.eval(construct{ @@ -51,6 +54,31 @@ object NewrelicSecurityClientMiddleware { } yield newRes } + private def addSecurityHeaders[F[_] : Async](request: Request[F], operation: AbstractOperation): Request[F] = { + val outboundRequest = new OutboundRequest(request) + if (operation != null) { + val securityMetaData = NewRelicSecurity.getAgent.getSecurityMetaData + val iastHeader = NewRelicSecurity.getAgent.getSecurityMetaData.getFuzzRequestIdentifier.getRaw + if (iastHeader != null && iastHeader.trim.nonEmpty) { + outboundRequest.setHeader(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID, iastHeader) + } + val csecParentId = securityMetaData.getCustomAttribute(GenericHelper.CSEC_PARENT_ID, classOf[String]) + if (StringUtils.isNotBlank(csecParentId)) { + outboundRequest.setHeader(GenericHelper.CSEC_PARENT_ID, csecParentId) + } + try { + NewRelicSecurity.getAgent.getSecurityMetaData.getMetaData.setFromJumpRequiredInStackTrace(Integer.valueOf(4)) + NewRelicSecurity.getAgent.registerOperation(operation) + } + finally { + if (operation.getApiID != null && operation.getApiID.trim.nonEmpty && operation.getExecutionId != null && operation.getExecutionId.trim.nonEmpty) { + outboundRequest.setHeader(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER, SSRFUtils.generateTracingHeaderValue(securityMetaData.getTracingHeaderValue, operation.getApiID, operation.getExecutionId, NewRelicSecurity.getAgent.getAgentUUID)) + } + } + } + outboundRequest.getRequest + } + def resource[F[_] : Async](delegate: Resource[F, Client[F]]): Resource[F, Client[F]] = { val res: Resource[F, Client[F]] = delegate.map(c =>clientResource(c)) res diff --git a/instrumentation-security/http4s-ember-client-2.13_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/OutboundRequest.scala b/instrumentation-security/http4s-ember-client-2.13_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/OutboundRequest.scala new file mode 100644 index 000000000..c19433953 --- /dev/null +++ b/instrumentation-security/http4s-ember-client-2.13_0.23/src/main/scala/com/newrelic/agent/security/instrumentation/http4s/ember/OutboundRequest.scala @@ -0,0 +1,20 @@ +package com.newrelic.agent.security.instrumentation.http4s.ember + +import org.http4s.{Header, Request} +import org.typelevel.ci.CIString + +/** + * Http4s's HttpRequest is immutable so we have to create a copy with the new headers. + */ + +class OutboundRequest[F[_]](request: Request[F]) { + private var req: Request[F] = request + + def setHeader(key: String, value: String): Unit = { + req = req.withHeaders(req.headers.put(Header.Raw.apply(CIString.apply(key), value))) + } + + def getRequest: Request[F] = { + req + } +} \ No newline at end of file From 3e22c036adb05ef68633c7b4040eaf0d3a2e962d Mon Sep 17 00:00:00 2001 From: idawda Date: Tue, 15 Oct 2024 12:56:06 +0530 Subject: [PATCH 06/14] update build.gradle --- .../http4s-ember-server-2.12_0.23/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/instrumentation-security/http4s-ember-server-2.12_0.23/build.gradle b/instrumentation-security/http4s-ember-server-2.12_0.23/build.gradle index c2aa21729..e6281a986 100644 --- a/instrumentation-security/http4s-ember-server-2.12_0.23/build.gradle +++ b/instrumentation-security/http4s-ember-server-2.12_0.23/build.gradle @@ -7,7 +7,7 @@ dependencies { implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}") implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}") implementation("org.scala-lang:scala-library:2.12.14") - implementation('org.http4s:http4s-ember-client_2.12:0.23.12') + implementation('org.http4s:http4s-ember-server_2.12:0.23.12') implementation("org.typelevel:cats-effect_2.12:3.3.0") testImplementation("org.http4s:http4s-dsl_2.12:0.23.12") } @@ -19,7 +19,7 @@ jar { } verifyInstrumentation { - passes 'org.http4s:http4s-ember-client_2.12:[0.23.0,0.24.0)' + passes 'org.http4s:http4s-ember-server_2.12:[0.23.0,0.24.0)' excludeRegex '.*(RC|M)[0-9]*' } From 2d1659ebb7a611a73f1916e54e713609547ba584 Mon Sep 17 00:00:00 2001 From: idawda Date: Wed, 6 Nov 2024 18:49:52 +0530 Subject: [PATCH 07/14] NR-332539: Unit tests for http4s-ember client instrumentation support --- .../blaze/client/EmberClientTest.scala | 87 +++++++++++++++++++ .../blaze/client/EmberClientTest.scala | 87 +++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 instrumentation-security/http4s-ember-client-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/EmberClientTest.scala create mode 100644 instrumentation-security/http4s-ember-client-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/EmberClientTest.scala diff --git a/instrumentation-security/http4s-ember-client-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/EmberClientTest.scala b/instrumentation-security/http4s-ember-client-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/EmberClientTest.scala new file mode 100644 index 000000000..ccb26c514 --- /dev/null +++ b/instrumentation-security/http4s-ember-client-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/EmberClientTest.scala @@ -0,0 +1,87 @@ +package com.nr.agent.security.instrumentation.blaze.client + +import cats.effect.unsafe.implicits.global +import cats.effect.{Async, IO} +import com.newrelic.agent.security.introspec.internal.HttpServerRule +import com.newrelic.agent.security.introspec.{InstrumentationTestConfig, SecurityInstrumentationTestRunner, SecurityIntrospector} +import com.newrelic.api.agent.security.instrumentation.helpers.{GenericHelper, ServletHelper} +import com.newrelic.api.agent.security.schema.operation.SSRFOperation +import com.newrelic.api.agent.security.schema.{AbstractOperation, VulnerabilityCaseType} +import com.nr.agent.security.instrumentation.blaze.client.Http4sTestUtils.makeRequest +import org.http4s.ember.client.EmberClientBuilder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.{Assert, FixMethodOrder, Rule, Test} + +import java.util +import java.util.UUID +import scala.concurrent.duration.DurationInt + +@RunWith(classOf[SecurityInstrumentationTestRunner]) +@InstrumentationTestConfig(includePrefixes = Array("org.http4s", "com.newrelic.agent.security.instrumentation.http4s")) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class EmberClientTest { + + @Rule + def server: HttpServerRule = httpServer + + val httpServer = new HttpServerRule() + + @Test + def blazeClientTest(): Unit = { + + val introspector: SecurityIntrospector = SecurityInstrumentationTestRunner.getIntrospector + makeRequest[IO](s"${server.getEndPoint}").unsafeRunTimed(2.seconds) + assertSSRFOperation(introspector.getOperations) + } + + @Test + def blazeClientTestWithHeaders(): Unit = { + val headerValue = String.valueOf(UUID.randomUUID) + + val introspector: SecurityIntrospector = SecurityInstrumentationTestRunner.getIntrospector + setCSECHeaders(headerValue = headerValue, introspector = introspector) + makeRequest[IO](s"${server.getEndPoint}").unsafeRunTimed(2.seconds) + assertSSRFOperation(introspector.getOperations) + verifyHeaders(headerValue, httpServer.getHeaders) + } + + + private def assertSSRFOperation(operations: util.List[AbstractOperation]): Unit = { + Assert.assertTrue("Incorrect number of operations detected!", operations.size == 1) + Assert.assertTrue("SSRFOperation not found!", operations.get(0).isInstanceOf[SSRFOperation]) + val operation: SSRFOperation = operations.get(0).asInstanceOf[SSRFOperation] + + Assert.assertFalse("operation should not be empty", operation.isEmpty) + Assert.assertFalse("JNDILookup should be false", operation.isJNDILookup) + Assert.assertFalse("LowSeverityHook should be disabled", operation.isLowSeverityHook) + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType) + Assert.assertEquals("Invalid executed method name.", "run", operation.getMethodName) + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint.toString, operation.getArg) + } + + private def verifyHeaders(headerValue: String, headers: util.Map[String, String]): Unit = { + Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)) + Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue + "a", headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)) + Assert.assertTrue(String.format("Missing CSEC header: %s", GenericHelper.CSEC_PARENT_ID), headers.containsKey(GenericHelper.CSEC_PARENT_ID)) + Assert.assertEquals(String.format("Invalid CSEC header value for: %s", GenericHelper.CSEC_PARENT_ID), headerValue + "b", headers.get(GenericHelper.CSEC_PARENT_ID)) + Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase)) + Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", headerValue), headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase)) + } + + private def setCSECHeaders(headerValue: String, introspector: SecurityIntrospector): Unit = { + introspector.setK2FuzzRequestId(headerValue + "a") + introspector.setK2ParentId(headerValue + "b") + introspector.setK2TracingData(headerValue) + } +} + +object Http4sTestUtils { + def makeRequest[F[_]: Async](url: String): F[String] = { + val client = EmberClientBuilder.default[F].build + client.use { c => + c.expect[String](url) + } + } +} + diff --git a/instrumentation-security/http4s-ember-client-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/EmberClientTest.scala b/instrumentation-security/http4s-ember-client-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/EmberClientTest.scala new file mode 100644 index 000000000..ccb26c514 --- /dev/null +++ b/instrumentation-security/http4s-ember-client-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/blaze/client/EmberClientTest.scala @@ -0,0 +1,87 @@ +package com.nr.agent.security.instrumentation.blaze.client + +import cats.effect.unsafe.implicits.global +import cats.effect.{Async, IO} +import com.newrelic.agent.security.introspec.internal.HttpServerRule +import com.newrelic.agent.security.introspec.{InstrumentationTestConfig, SecurityInstrumentationTestRunner, SecurityIntrospector} +import com.newrelic.api.agent.security.instrumentation.helpers.{GenericHelper, ServletHelper} +import com.newrelic.api.agent.security.schema.operation.SSRFOperation +import com.newrelic.api.agent.security.schema.{AbstractOperation, VulnerabilityCaseType} +import com.nr.agent.security.instrumentation.blaze.client.Http4sTestUtils.makeRequest +import org.http4s.ember.client.EmberClientBuilder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.{Assert, FixMethodOrder, Rule, Test} + +import java.util +import java.util.UUID +import scala.concurrent.duration.DurationInt + +@RunWith(classOf[SecurityInstrumentationTestRunner]) +@InstrumentationTestConfig(includePrefixes = Array("org.http4s", "com.newrelic.agent.security.instrumentation.http4s")) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class EmberClientTest { + + @Rule + def server: HttpServerRule = httpServer + + val httpServer = new HttpServerRule() + + @Test + def blazeClientTest(): Unit = { + + val introspector: SecurityIntrospector = SecurityInstrumentationTestRunner.getIntrospector + makeRequest[IO](s"${server.getEndPoint}").unsafeRunTimed(2.seconds) + assertSSRFOperation(introspector.getOperations) + } + + @Test + def blazeClientTestWithHeaders(): Unit = { + val headerValue = String.valueOf(UUID.randomUUID) + + val introspector: SecurityIntrospector = SecurityInstrumentationTestRunner.getIntrospector + setCSECHeaders(headerValue = headerValue, introspector = introspector) + makeRequest[IO](s"${server.getEndPoint}").unsafeRunTimed(2.seconds) + assertSSRFOperation(introspector.getOperations) + verifyHeaders(headerValue, httpServer.getHeaders) + } + + + private def assertSSRFOperation(operations: util.List[AbstractOperation]): Unit = { + Assert.assertTrue("Incorrect number of operations detected!", operations.size == 1) + Assert.assertTrue("SSRFOperation not found!", operations.get(0).isInstanceOf[SSRFOperation]) + val operation: SSRFOperation = operations.get(0).asInstanceOf[SSRFOperation] + + Assert.assertFalse("operation should not be empty", operation.isEmpty) + Assert.assertFalse("JNDILookup should be false", operation.isJNDILookup) + Assert.assertFalse("LowSeverityHook should be disabled", operation.isLowSeverityHook) + Assert.assertEquals("Invalid event category.", VulnerabilityCaseType.HTTP_REQUEST, operation.getCaseType) + Assert.assertEquals("Invalid executed method name.", "run", operation.getMethodName) + Assert.assertEquals("Invalid executed parameters.", server.getEndPoint.toString, operation.getArg) + } + + private def verifyHeaders(headerValue: String, headers: util.Map[String, String]): Unit = { + Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headers.containsKey(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)) + Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID), headerValue + "a", headers.get(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID)) + Assert.assertTrue(String.format("Missing CSEC header: %s", GenericHelper.CSEC_PARENT_ID), headers.containsKey(GenericHelper.CSEC_PARENT_ID)) + Assert.assertEquals(String.format("Invalid CSEC header value for: %s", GenericHelper.CSEC_PARENT_ID), headerValue + "b", headers.get(GenericHelper.CSEC_PARENT_ID)) + Assert.assertTrue(String.format("Missing CSEC header: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase)) + Assert.assertEquals(String.format("Invalid CSEC header value for: %s", ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER), String.format("%s;DUMMY_UUID/dummy-api-id/dummy-exec-id;", headerValue), headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase)) + } + + private def setCSECHeaders(headerValue: String, introspector: SecurityIntrospector): Unit = { + introspector.setK2FuzzRequestId(headerValue + "a") + introspector.setK2ParentId(headerValue + "b") + introspector.setK2TracingData(headerValue) + } +} + +object Http4sTestUtils { + def makeRequest[F[_]: Async](url: String): F[String] = { + val client = EmberClientBuilder.default[F].build + client.use { c => + c.expect[String](url) + } + } +} + From 2e85a53b8dd24bfad88e202211650ee7a6341ec3 Mon Sep 17 00:00:00 2001 From: idawda Date: Fri, 8 Nov 2024 15:36:08 +0530 Subject: [PATCH 08/14] code optimisation and clean up in ember server --- .../EmberUtils.java | 67 ------------------- .../RequestProcessor.scala | 45 +++++++++---- .../build.gradle | 2 +- .../EmberUtils.java | 67 ------------------- .../RequestProcessor.scala | 44 ++++++++---- 5 files changed, 62 insertions(+), 163 deletions(-) delete mode 100644 instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/EmberUtils.java delete mode 100644 instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/EmberUtils.java diff --git a/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/EmberUtils.java b/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/EmberUtils.java deleted file mode 100644 index ac19a397b..000000000 --- a/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/EmberUtils.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.newrelic.agent.security.http4s.ember.server; - -import com.newrelic.api.agent.security.NewRelicSecurity; -import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; -import com.newrelic.api.agent.security.schema.StringUtils; - -import java.util.Map; - -public class EmberUtils { - - public static String getContentType(Map headers) { - String contentType = StringUtils.EMPTY; - if (headers.containsKey("content-type")){ - contentType = headers.get("content-type"); - } - return contentType; - } - - public static String getTraceHeader(Map headers) { - String 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()); - } - } - return data; - } - - public static String getProtocol(boolean isSecure) { - if (isSecure) { - return "https"; - } - return "http"; - } - - - private static boolean isLockAcquired() { - try { - return NewRelicSecurity.isHookProcessingActive() && - Boolean.TRUE.equals(NewRelicSecurity.getAgent().getSecurityMetaData().getCustomAttribute(getNrSecCustomAttribName(), Boolean.class)); - } catch (Throwable ignored) {} - return false; - } - - public static boolean acquireLockIfPossible() { - try { - if (NewRelicSecurity.isHookProcessingActive() && !isLockAcquired()) { - NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(getNrSecCustomAttribName(), true); - return true; - } - } catch (Throwable ignored){} - return false; - } - - public static void releaseLock() { - try { - if(NewRelicSecurity.isHookProcessingActive()) { - NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(getNrSecCustomAttribName(), null); - } - } catch (Throwable ignored){} - } - - private static String getNrSecCustomAttribName() { - return "HTTP4S-EMBER-REQUEST_LOCK" + Thread.currentThread().getId(); - } -} diff --git a/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala b/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala index 6b9bfbd9c..5386c67f3 100644 --- a/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala +++ b/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala @@ -30,16 +30,16 @@ object RequestProcessor { val result = construct((): Unit) .redeemWith(_ => httpApp(request), _ => for { - _ <- preprocessHttpRequest(request) + isLockAcquired <- preprocessHttpRequest(request) resp <- httpApp(request) - _ <- postProcessSecurityHook(resp) + _ <- postProcessSecurityHook(isLockAcquired, resp) } yield resp ) result } - private def preprocessHttpRequest[F[_]: Sync](request: Request[F]): F[Unit] = construct { - val isLockAcquired = EmberUtils.acquireLockIfPossible() + private def preprocessHttpRequest[F[_]: Sync](request: Request[F]): F[Boolean] = construct { + val isLockAcquired = GenericHelper.acquireLockIfPossible("HTTP4S-EMBER-REQUEST_LOCK", request.hashCode()) try { if (NewRelicSecurity.isHookProcessingActive && isLockAcquired && !NewRelicSecurity.getAgent.getSecurityMetaData.getRequest.isRequestParsed){ @@ -50,7 +50,12 @@ object RequestProcessor { securityRequest.setMethod(request.method.name) securityRequest.setServerPort((request.serverPort).get.asInstanceOf[Port].value) securityRequest.setClientIP(request.remoteAddr.get.toString) - securityRequest.setProtocol(EmberUtils.getProtocol(request.isSecure.get)) + if(request.isSecure.get){ + securityRequest.setProtocol("https") + } else { + securityRequest.setProtocol("http") + } + securityRequest.setUrl(request.uri.toString) if (securityRequest.getClientIP != null && securityRequest.getClientIP.trim.nonEmpty) { @@ -59,8 +64,8 @@ object RequestProcessor { } processRequestHeaders(request.headers, securityRequest) - securityMetaData.setTracingHeaderValue(EmberUtils.getTraceHeader(securityRequest.getHeaders)) - securityRequest.setContentType(EmberUtils.getContentType(securityRequest.getHeaders)) + securityMetaData.setTracingHeaderValue(getTraceHeader(securityRequest.getHeaders)) + securityRequest.setContentType(getContentType(securityRequest.getHeaders)) // TODO extract request body & user class detection @@ -71,20 +76,17 @@ object RequestProcessor { } 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) - } finally { - if (isLockAcquired) { - EmberUtils.releaseLock() - } } + isLockAcquired } - private def postProcessSecurityHook[F[_]: Sync](response: Response[F]): F[Unit] = construct { + private def postProcessSecurityHook[F[_]: Sync](isLockAcquired: Boolean, response: Response[F]): F[Unit] = construct { try { - if (NewRelicSecurity.isHookProcessingActive) { + if (isLockAcquired && NewRelicSecurity.isHookProcessingActive) { val securityResponse = NewRelicSecurity.getAgent.getSecurityMetaData.getResponse securityResponse.setResponseCode(response.status.code) processResponseHeaders(response.headers, securityResponse) - securityResponse.setResponseContentType(EmberUtils.getContentType(securityResponse.getHeaders)) + securityResponse.setResponseContentType(getContentType(securityResponse.getHeaders)) // TODO extract response body @@ -154,5 +156,20 @@ object RequestProcessor { }) } + private def getContentType(headers: util.Map[String, String]): String = { + var contentType = StringUtils.EMPTY + if (headers.containsKey("content-type")) contentType = headers.get("content-type") + contentType + } + + private def getTraceHeader(headers: util.Map[String, String]): String = { + var data = StringUtils.EMPTY + if (headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER) || headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase)) { + data = headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER) + if (data == null || data.trim.isEmpty) data = headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase) + } + data + } + private def construct[F[_] : Sync, T](t: => T): F[T] = Sync[F].delay(t) } diff --git a/instrumentation-security/http4s-ember-server-2.13_0.23/build.gradle b/instrumentation-security/http4s-ember-server-2.13_0.23/build.gradle index 704c8d2eb..6f9c24c92 100644 --- a/instrumentation-security/http4s-ember-server-2.13_0.23/build.gradle +++ b/instrumentation-security/http4s-ember-server-2.13_0.23/build.gradle @@ -9,7 +9,7 @@ dependencies { implementation("org.scala-lang:scala-library:2.13.3") implementation('org.http4s:http4s-ember-server_2.13:0.23.12') implementation("org.typelevel:cats-effect_2.13:3.3.12"){transitive = false} - testImplementation("org.http4s:http4s-dsl_2.12:0.23.12") + testImplementation("org.http4s:http4s-dsl_2.13:0.23.12") } jar { diff --git a/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/EmberUtils.java b/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/EmberUtils.java deleted file mode 100644 index ac19a397b..000000000 --- a/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/EmberUtils.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.newrelic.agent.security.http4s.ember.server; - -import com.newrelic.api.agent.security.NewRelicSecurity; -import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper; -import com.newrelic.api.agent.security.schema.StringUtils; - -import java.util.Map; - -public class EmberUtils { - - public static String getContentType(Map headers) { - String contentType = StringUtils.EMPTY; - if (headers.containsKey("content-type")){ - contentType = headers.get("content-type"); - } - return contentType; - } - - public static String getTraceHeader(Map headers) { - String 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()); - } - } - return data; - } - - public static String getProtocol(boolean isSecure) { - if (isSecure) { - return "https"; - } - return "http"; - } - - - private static boolean isLockAcquired() { - try { - return NewRelicSecurity.isHookProcessingActive() && - Boolean.TRUE.equals(NewRelicSecurity.getAgent().getSecurityMetaData().getCustomAttribute(getNrSecCustomAttribName(), Boolean.class)); - } catch (Throwable ignored) {} - return false; - } - - public static boolean acquireLockIfPossible() { - try { - if (NewRelicSecurity.isHookProcessingActive() && !isLockAcquired()) { - NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(getNrSecCustomAttribName(), true); - return true; - } - } catch (Throwable ignored){} - return false; - } - - public static void releaseLock() { - try { - if(NewRelicSecurity.isHookProcessingActive()) { - NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(getNrSecCustomAttribName(), null); - } - } catch (Throwable ignored){} - } - - private static String getNrSecCustomAttribName() { - return "HTTP4S-EMBER-REQUEST_LOCK" + Thread.currentThread().getId(); - } -} diff --git a/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala b/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala index 88de7997b..ff030e634 100644 --- a/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala +++ b/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala @@ -30,16 +30,16 @@ object RequestProcessor { val result = construct((): Unit) .redeemWith(_ => httpApp(request), _ => for { - _ <- preprocessHttpRequest(request) + isLockAcquired <- preprocessHttpRequest(request) resp <- httpApp(request) - _ <- postProcessSecurityHook(resp) + _ <- postProcessSecurityHook(isLockAcquired, resp) } yield resp ) result } - private def preprocessHttpRequest[F[_]: Sync](request: Request[F]): F[Unit] = construct { - val isLockAcquired = EmberUtils.acquireLockIfPossible() + private def preprocessHttpRequest[F[_]: Sync](request: Request[F]): F[Boolean] = construct { + val isLockAcquired = GenericHelper.acquireLockIfPossible("HTTP4S-EMBER-REQUEST_LOCK", request.hashCode()) try { if (NewRelicSecurity.isHookProcessingActive && isLockAcquired && !NewRelicSecurity.getAgent.getSecurityMetaData.getRequest.isRequestParsed){ @@ -50,7 +50,11 @@ object RequestProcessor { securityRequest.setMethod(request.method.name) securityRequest.setServerPort((request.serverPort).get.asInstanceOf[Port].value) securityRequest.setClientIP(request.remoteAddr.get.toString) - securityRequest.setProtocol(EmberUtils.getProtocol(request.isSecure.get)) + if(request.isSecure.get){ + securityRequest.setProtocol("https") + } else { + securityRequest.setProtocol("http") + } securityRequest.setUrl(request.uri.toString) if (securityRequest.getClientIP != null && securityRequest.getClientIP.trim.nonEmpty) { @@ -59,8 +63,8 @@ object RequestProcessor { } processRequestHeaders(request.headers, securityRequest) - securityMetaData.setTracingHeaderValue(EmberUtils.getTraceHeader(securityRequest.getHeaders)) - securityRequest.setContentType(EmberUtils.getContentType(securityRequest.getHeaders)) + securityMetaData.setTracingHeaderValue(getTraceHeader(securityRequest.getHeaders)) + securityRequest.setContentType(getContentType(securityRequest.getHeaders)) // TODO extract request body & user class detection @@ -71,20 +75,17 @@ object RequestProcessor { } catch { case e: Throwable => NewRelicSecurity.getAgent.log(LogLevel.WARNING, String.format(GenericHelper.ERROR_GENERATING_HTTP_REQUEST, HTTP_4S_EMBER_SERVER_2_13_0_23, e.getMessage), e, this.getClass.getName) - } finally { - if (isLockAcquired) { - EmberUtils.releaseLock() - } } + isLockAcquired } - private def postProcessSecurityHook[F[_]: Sync](response: Response[F]): F[Unit] = construct { + private def postProcessSecurityHook[F[_]: Sync](isLockAcquired: Boolean, response: Response[F]): F[Unit] = construct { try { - if (NewRelicSecurity.isHookProcessingActive) { + if (isLockAcquired && NewRelicSecurity.isHookProcessingActive) { val securityResponse = NewRelicSecurity.getAgent.getSecurityMetaData.getResponse securityResponse.setResponseCode(response.status.code) processResponseHeaders(response.headers, securityResponse) - securityResponse.setResponseContentType(EmberUtils.getContentType(securityResponse.getHeaders)) + securityResponse.setResponseContentType(getContentType(securityResponse.getHeaders)) // TODO extract response body @@ -154,5 +155,20 @@ object RequestProcessor { }) } + private def getContentType(headers: util.Map[String, String]): String = { + var contentType = StringUtils.EMPTY + if (headers.containsKey("content-type")) contentType = headers.get("content-type") + contentType + } + + private def getTraceHeader(headers: util.Map[String, String]): String = { + var data = StringUtils.EMPTY + if (headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER) || headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase)) { + data = headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER) + if (data == null || data.trim.isEmpty) data = headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase) + } + data + } + private def construct[F[_] : Sync, T](t: => T): F[T] = Sync[F].delay(t) } From 86d1beef88cb5f8d2d4c439ace4123d7773f8adc Mon Sep 17 00:00:00 2001 From: idawda Date: Fri, 8 Nov 2024 16:05:48 +0530 Subject: [PATCH 09/14] NR-333390: Unit tests for http-ember server instrumentation --- .../ember/server/EmberServerBuilderTest.scala | 119 ++++++++++++++++++ .../ember/server/Http4sTestServer.scala | 30 +++++ .../ember/server/EmberServerBuilderTest.scala | 118 +++++++++++++++++ .../ember/server/Http4sTestServer.scala | 30 +++++ 4 files changed, 297 insertions(+) create mode 100644 instrumentation-security/http4s-ember-server-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/EmberServerBuilderTest.scala create mode 100644 instrumentation-security/http4s-ember-server-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/Http4sTestServer.scala create mode 100644 instrumentation-security/http4s-ember-server-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/EmberServerBuilderTest.scala create mode 100644 instrumentation-security/http4s-ember-server-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/Http4sTestServer.scala diff --git a/instrumentation-security/http4s-ember-server-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/EmberServerBuilderTest.scala b/instrumentation-security/http4s-ember-server-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/EmberServerBuilderTest.scala new file mode 100644 index 000000000..54664be50 --- /dev/null +++ b/instrumentation-security/http4s-ember-server-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/EmberServerBuilderTest.scala @@ -0,0 +1,119 @@ +package com.nr.agent.security.instrumentation.ember.server + +import cats.effect.IO +import com.newrelic.agent.security.introspec.{InstrumentationTestConfig, SecurityInstrumentationTestRunner, SecurityIntrospector} +import com.newrelic.api.agent.security.instrumentation.helpers.{GenericHelper, ServletHelper} +import com.newrelic.api.agent.security.schema.{SecurityMetaData, VulnerabilityCaseType} +import com.newrelic.api.agent.security.schema.operation.RXSSOperation +import org.http4s.HttpRoutes +import org.http4s.Method.GET +import org.http4s.dsl.io._ +import org.http4s.implicits._ +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.ember", "com.newrelic.agent.security.http4s.ember.server", "scala")) +class EmberServerBuilderTest { + + val hostname = "0.0.0.0" + val port: Int = SecurityInstrumentationTestRunner.getIntrospector.getRandomPort + val contentType: String = "text/plain" + + val emberServer = new Http4sTestServer(hostname, port, + HttpRoutes.of[IO] { + case GET -> Root / "hello" / name => + Ok(s"Hello, $name.") + }.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("response should not be empty", operation.getResponse.isEmpty) + // Assert.assertEquals("Invalid response content-type.", contentType, operation.getResponse.getResponseContentType) + } + + 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.assertFalse("response should not be empty", metaData.getResponse.isEmpty) + // Assert.assertEquals("Invalid response content-type.", contentType, metaData.getResponse.getResponseContentType) + } +} + +object Http4sTestUtils { + def makeRequest(url: String, addCSECHeader: Boolean, headerValue: String): Int = { + val u: URL = new URL(url) + val conn = u.openConnection.asInstanceOf[HttpURLConnection] + conn.setRequestProperty("content-type", "text/plain; charset=utf-8") + 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)) + } + conn.connect() + conn.getResponseCode + } +} + diff --git a/instrumentation-security/http4s-ember-server-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/Http4sTestServer.scala b/instrumentation-security/http4s-ember-server-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/Http4sTestServer.scala new file mode 100644 index 000000000..d1eed636d --- /dev/null +++ b/instrumentation-security/http4s-ember-server-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/Http4sTestServer.scala @@ -0,0 +1,30 @@ +package com.nr.agent.security.instrumentation.ember.server + +import cats.effect.{IO, Resource} +import com.comcast.ip4s._ +import org.http4s.HttpApp +import org.http4s.server.Server +import org.http4s.ember.server.EmberServerBuilder +import cats.effect.unsafe.implicits.global + +class Http4sTestServer(val testServerHost: String, val port: Int, val httpApp: HttpApp[IO]) { + + var server: Server = _ + var finalizer: IO[Unit] = _ + + val serverResource: Resource[IO, Server] = EmberServerBuilder.default[IO] + .withHttpApp(httpApp) + .withHost(Host.fromString(testServerHost).orNull) + .withPort(Port.fromInt(port).get) + .build + + def start(): Unit = { + val materializedServer = serverResource.allocated.unsafeRunSync() + server = materializedServer._1 + finalizer = materializedServer._2 + } + + def stop(): Unit = finalizer.unsafeRunSync() + + def hostname: String = server.address.getHostName +} diff --git a/instrumentation-security/http4s-ember-server-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/EmberServerBuilderTest.scala b/instrumentation-security/http4s-ember-server-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/EmberServerBuilderTest.scala new file mode 100644 index 000000000..099f8a03d --- /dev/null +++ b/instrumentation-security/http4s-ember-server-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/EmberServerBuilderTest.scala @@ -0,0 +1,118 @@ +package com.nr.agent.security.instrumentation.ember.server + +import cats.effect.IO +import com.newrelic.agent.security.introspec.{InstrumentationTestConfig, SecurityInstrumentationTestRunner, SecurityIntrospector} +import com.newrelic.api.agent.security.instrumentation.helpers.{GenericHelper, ServletHelper} +import com.newrelic.api.agent.security.schema.operation.RXSSOperation +import com.newrelic.api.agent.security.schema.{SecurityMetaData, VulnerabilityCaseType} +import org.http4s.HttpRoutes +import org.http4s.Method.GET +import org.http4s.dsl.io._ +import org.http4s.implicits._ +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.ember.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 GET -> Root / "hello" / name => + Ok(s"Hello, $name.") + }.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("response should not be empty", operation.getResponse.isEmpty) +// Assert.assertEquals("Invalid response content-type.", contentType, operation.getResponse.getResponseContentType) + } + + 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.assertFalse("response should not be empty", metaData.getResponse.isEmpty) +// Assert.assertEquals("Invalid response content-type.", contentType, metaData.getResponse.getResponseContentType) + } +} + +object Http4sTestUtils { + def makeRequest(url: String, addCSECHeader: Boolean, headerValue: String): Int = { + val u: URL = new URL(url) + val conn = u.openConnection.asInstanceOf[HttpURLConnection] + conn.setRequestProperty("content-type", "text/plain; charset=utf-8") + 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)) + } + conn.connect() + conn.getResponseCode + } +} diff --git a/instrumentation-security/http4s-ember-server-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/Http4sTestServer.scala b/instrumentation-security/http4s-ember-server-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/Http4sTestServer.scala new file mode 100644 index 000000000..d1eed636d --- /dev/null +++ b/instrumentation-security/http4s-ember-server-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/Http4sTestServer.scala @@ -0,0 +1,30 @@ +package com.nr.agent.security.instrumentation.ember.server + +import cats.effect.{IO, Resource} +import com.comcast.ip4s._ +import org.http4s.HttpApp +import org.http4s.server.Server +import org.http4s.ember.server.EmberServerBuilder +import cats.effect.unsafe.implicits.global + +class Http4sTestServer(val testServerHost: String, val port: Int, val httpApp: HttpApp[IO]) { + + var server: Server = _ + var finalizer: IO[Unit] = _ + + val serverResource: Resource[IO, Server] = EmberServerBuilder.default[IO] + .withHttpApp(httpApp) + .withHost(Host.fromString(testServerHost).orNull) + .withPort(Port.fromInt(port).get) + .build + + def start(): Unit = { + val materializedServer = serverResource.allocated.unsafeRunSync() + server = materializedServer._1 + finalizer = materializedServer._2 + } + + def stop(): Unit = finalizer.unsafeRunSync() + + def hostname: String = server.address.getHostName +} From 12d1c7c44b52df3ab19196e4ae325097c68d78ba Mon Sep 17 00:00:00 2001 From: idawda Date: Tue, 19 Nov 2024 15:36:00 +0530 Subject: [PATCH 10/14] NR-293844: Extract request and response body for http4s-ember server --- .../RequestProcessor.scala | 23 +++++++++++-------- .../RequestProcessor.scala | 22 +++++++++++------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala b/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala index 5386c67f3..2dfb12f24 100644 --- a/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala +++ b/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala @@ -11,7 +11,7 @@ import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityExcepti import com.newrelic.api.agent.security.schema.operation.RXSSOperation import com.newrelic.api.agent.security.schema.policy.AgentPolicy import com.newrelic.api.agent.security.utils.logging.LogLevel -import org.http4s.{Headers, Request, Response} +import org.http4s.{Headers, Message, Request, Response} import java.util @@ -30,18 +30,20 @@ object RequestProcessor { val result = construct((): Unit) .redeemWith(_ => httpApp(request), _ => for { - isLockAcquired <- preprocessHttpRequest(request) + requestBody <- extractBody(request) + isLockAcquired <- preprocessHttpRequest(request, requestBody) resp <- httpApp(request) - _ <- postProcessSecurityHook(isLockAcquired, resp) + responseBody <- extractBody(resp) + _ <- postProcessSecurityHook(isLockAcquired, resp, responseBody) } yield resp ) result } - private def preprocessHttpRequest[F[_]: Sync](request: Request[F]): F[Boolean] = construct { + private def preprocessHttpRequest[F[_]: Sync](request: Request[F], body: String): F[Boolean] = construct { val isLockAcquired = GenericHelper.acquireLockIfPossible("HTTP4S-EMBER-REQUEST_LOCK", request.hashCode()) try { - if (NewRelicSecurity.isHookProcessingActive && isLockAcquired && !NewRelicSecurity.getAgent.getSecurityMetaData.getRequest.isRequestParsed){ + if (isLockAcquired && !NewRelicSecurity.getAgent.getSecurityMetaData.getRequest.isRequestParsed){ val securityMetaData: SecurityMetaData = NewRelicSecurity.getAgent.getSecurityMetaData val securityRequest: HttpRequest = securityMetaData.getRequest @@ -66,8 +68,7 @@ object RequestProcessor { processRequestHeaders(request.headers, securityRequest) securityMetaData.setTracingHeaderValue(getTraceHeader(securityRequest.getHeaders)) securityRequest.setContentType(getContentType(securityRequest.getHeaders)) - - // TODO extract request body & user class detection + securityRequest.getBody.append(body) val trace: Array[StackTraceElement] = Thread.currentThread.getStackTrace securityMetaData.getMetaData.setServiceTrace(util.Arrays.copyOfRange(trace, 1, trace.length)) @@ -80,7 +81,7 @@ object RequestProcessor { isLockAcquired } - private def postProcessSecurityHook[F[_]: Sync](isLockAcquired: Boolean, response: Response[F]): F[Unit] = construct { + private def postProcessSecurityHook[F[_]: Sync](isLockAcquired: Boolean, response: Response[F], body: String): F[Unit] = construct { try { if (isLockAcquired && NewRelicSecurity.isHookProcessingActive) { val securityResponse = NewRelicSecurity.getAgent.getSecurityMetaData.getResponse @@ -88,7 +89,7 @@ object RequestProcessor { processResponseHeaders(response.headers, securityResponse) securityResponse.setResponseContentType(getContentType(securityResponse.getHeaders)) - // TODO extract response body + securityResponse.getResponseBody.append(body) ServletHelper.executeBeforeExitingTransaction() if (!ServletHelper.isResponseContentTypeExcluded(NewRelicSecurity.getAgent.getSecurityMetaData.getResponse.getResponseContentType)) { @@ -107,6 +108,10 @@ object RequestProcessor { } } + private def extractBody[F[_]: Sync](msg: Message[F]): F[String] = { + msg.bodyText.compile.string + } + private def processRequestHeaders(headers: Headers, securityRequest: HttpRequest): Unit = { headers.foreach(header => { var takeNextValue = false diff --git a/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala b/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala index ff030e634..ca5252e30 100644 --- a/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala +++ b/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala @@ -11,7 +11,7 @@ import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityExcepti import com.newrelic.api.agent.security.schema.operation.RXSSOperation import com.newrelic.api.agent.security.schema.policy.AgentPolicy import com.newrelic.api.agent.security.utils.logging.LogLevel -import org.http4s.{Headers, Request, Response} +import org.http4s.{Headers, Message, Request, Response} import java.util @@ -30,18 +30,20 @@ object RequestProcessor { val result = construct((): Unit) .redeemWith(_ => httpApp(request), _ => for { - isLockAcquired <- preprocessHttpRequest(request) + requestBody <- extractBody(request) + isLockAcquired <- preprocessHttpRequest(request, requestBody) resp <- httpApp(request) - _ <- postProcessSecurityHook(isLockAcquired, resp) + responseBody <- extractBody(resp) + _ <- postProcessSecurityHook(isLockAcquired, resp, responseBody) } yield resp ) result } - private def preprocessHttpRequest[F[_]: Sync](request: Request[F]): F[Boolean] = construct { + private def preprocessHttpRequest[F[_]: Sync](request: Request[F], body: String): F[Boolean] = construct { val isLockAcquired = GenericHelper.acquireLockIfPossible("HTTP4S-EMBER-REQUEST_LOCK", request.hashCode()) try { - if (NewRelicSecurity.isHookProcessingActive && isLockAcquired && !NewRelicSecurity.getAgent.getSecurityMetaData.getRequest.isRequestParsed){ + if (isLockAcquired && !NewRelicSecurity.getAgent.getSecurityMetaData.getRequest.isRequestParsed){ val securityMetaData: SecurityMetaData = NewRelicSecurity.getAgent.getSecurityMetaData val securityRequest: HttpRequest = securityMetaData.getRequest @@ -66,7 +68,7 @@ object RequestProcessor { securityMetaData.setTracingHeaderValue(getTraceHeader(securityRequest.getHeaders)) securityRequest.setContentType(getContentType(securityRequest.getHeaders)) - // TODO extract request body & user class detection + securityRequest.getBody.append(body) val trace: Array[StackTraceElement] = Thread.currentThread.getStackTrace securityMetaData.getMetaData.setServiceTrace(util.Arrays.copyOfRange(trace, 1, trace.length)) @@ -79,7 +81,11 @@ object RequestProcessor { isLockAcquired } - private def postProcessSecurityHook[F[_]: Sync](isLockAcquired: Boolean, response: Response[F]): F[Unit] = construct { + private def extractBody[F[_]: Sync](msg: Message[F]): F[String] = { + msg.bodyText.compile.string + } + + private def postProcessSecurityHook[F[_]: Sync](isLockAcquired: Boolean, response: Response[F], body: String): F[Unit] = construct { try { if (isLockAcquired && NewRelicSecurity.isHookProcessingActive) { val securityResponse = NewRelicSecurity.getAgent.getSecurityMetaData.getResponse @@ -87,7 +93,7 @@ object RequestProcessor { processResponseHeaders(response.headers, securityResponse) securityResponse.setResponseContentType(getContentType(securityResponse.getHeaders)) - // TODO extract response body + securityResponse.getResponseBody.append(body) ServletHelper.executeBeforeExitingTransaction() if (!ServletHelper.isResponseContentTypeExcluded(NewRelicSecurity.getAgent.getSecurityMetaData.getResponse.getResponseContentType)) { From b9cfda27c9b6edf2b2174ab061b535287be868f1 Mon Sep 17 00:00:00 2001 From: idawda Date: Tue, 19 Nov 2024 17:30:05 +0530 Subject: [PATCH 11/14] Extract request and response body by using the charset --- .../RequestProcessor.scala | 9 ++++++++- .../RequestProcessor.scala | 8 +++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala b/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala index 2dfb12f24..fd47e3026 100644 --- a/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala +++ b/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala @@ -11,6 +11,8 @@ import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityExcepti import com.newrelic.api.agent.security.schema.operation.RXSSOperation import com.newrelic.api.agent.security.schema.policy.AgentPolicy import com.newrelic.api.agent.security.utils.logging.LogLevel +import fs2.text.decodeWithCharset +import org.http4s.Charset.`UTF-8` import org.http4s.{Headers, Message, Request, Response} import java.util @@ -109,7 +111,12 @@ object RequestProcessor { } private def extractBody[F[_]: Sync](msg: Message[F]): F[String] = { - msg.bodyText.compile.string + if (msg.contentType.nonEmpty && msg.contentType.get.charset.nonEmpty) { + val charset = msg.contentType.get.charset.get; + msg.body.through(decodeWithCharset(charset.nioCharset)).compile.string + } else { + msg.bodyText.compile.string + } } private def processRequestHeaders(headers: Headers, securityRequest: HttpRequest): Unit = { diff --git a/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala b/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala index ca5252e30..16babe40a 100644 --- a/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala +++ b/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala @@ -11,6 +11,7 @@ import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityExcepti import com.newrelic.api.agent.security.schema.operation.RXSSOperation import com.newrelic.api.agent.security.schema.policy.AgentPolicy import com.newrelic.api.agent.security.utils.logging.LogLevel +import fs2.text.decodeWithCharset import org.http4s.{Headers, Message, Request, Response} import java.util @@ -82,7 +83,12 @@ object RequestProcessor { } private def extractBody[F[_]: Sync](msg: Message[F]): F[String] = { - msg.bodyText.compile.string + if (msg.contentType.nonEmpty && msg.contentType.get.charset.nonEmpty) { + val charset = msg.contentType.get.charset.get; + msg.body.through(decodeWithCharset(charset.nioCharset)).compile.string + } else { + msg.bodyText.compile.string + } } private def postProcessSecurityHook[F[_]: Sync](isLockAcquired: Boolean, response: Response[F], body: String): F[Unit] = construct { From d74f1ff65b736c925bf379b65cb126f26b92fc15 Mon Sep 17 00:00:00 2001 From: idawda Date: Tue, 19 Nov 2024 18:13:23 +0530 Subject: [PATCH 12/14] Extract request and response body by using the charset --- .../RequestProcessor.scala | 3 ++- .../RequestProcessor.scala | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala b/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala index fd47e3026..08fee63a2 100644 --- a/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala +++ b/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala @@ -11,6 +11,7 @@ import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityExcepti import com.newrelic.api.agent.security.schema.operation.RXSSOperation import com.newrelic.api.agent.security.schema.policy.AgentPolicy import com.newrelic.api.agent.security.utils.logging.LogLevel +import fs2.RaiseThrowable import fs2.text.decodeWithCharset import org.http4s.Charset.`UTF-8` import org.http4s.{Headers, Message, Request, Response} @@ -113,7 +114,7 @@ object RequestProcessor { 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.body.through(decodeWithCharset(charset.nioCharset)).compile.string + msg.bodyText(RaiseThrowable.fromApplicativeError, defaultCharset = charset).compile.string } else { msg.bodyText.compile.string } diff --git a/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala b/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala index 16babe40a..a20ba8989 100644 --- a/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala +++ b/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala @@ -11,6 +11,7 @@ import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityExcepti import com.newrelic.api.agent.security.schema.operation.RXSSOperation import com.newrelic.api.agent.security.schema.policy.AgentPolicy import com.newrelic.api.agent.security.utils.logging.LogLevel +import fs2.RaiseThrowable import fs2.text.decodeWithCharset import org.http4s.{Headers, Message, Request, Response} @@ -85,7 +86,7 @@ object RequestProcessor { 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.body.through(decodeWithCharset(charset.nioCharset)).compile.string + msg.bodyText(RaiseThrowable.fromApplicativeError, defaultCharset = charset).compile.string } else { msg.bodyText.compile.string } From 1b7d27da8d48b6c1f2fb418a0df464123a3d16b7 Mon Sep 17 00:00:00 2001 From: idawda Date: Thu, 5 Dec 2024 11:33:17 +0530 Subject: [PATCH 13/14] Do not generate RXSS events if RXSS is disabled --- .../RequestProcessor.scala | 4 +--- .../RequestProcessor.scala | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala b/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala index 08fee63a2..c648c61e4 100644 --- a/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala +++ b/instrumentation-security/http4s-ember-server-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala @@ -12,8 +12,6 @@ import com.newrelic.api.agent.security.schema.operation.RXSSOperation import com.newrelic.api.agent.security.schema.policy.AgentPolicy import com.newrelic.api.agent.security.utils.logging.LogLevel import fs2.RaiseThrowable -import fs2.text.decodeWithCharset -import org.http4s.Charset.`UTF-8` import org.http4s.{Headers, Message, Request, Response} import java.util @@ -86,7 +84,7 @@ object RequestProcessor { private def postProcessSecurityHook[F[_]: Sync](isLockAcquired: Boolean, response: Response[F], body: String): F[Unit] = construct { try { - if (isLockAcquired && NewRelicSecurity.isHookProcessingActive) { + if (isLockAcquired && NewRelicSecurity.isHookProcessingActive && !NewRelicSecurity.getAgent.getIastDetectionCategory.getRxssEnabled) { val securityResponse = NewRelicSecurity.getAgent.getSecurityMetaData.getResponse securityResponse.setResponseCode(response.status.code) processResponseHeaders(response.headers, securityResponse) diff --git a/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala b/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala index a20ba8989..536ecfc10 100644 --- a/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala +++ b/instrumentation-security/http4s-ember-server-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala @@ -12,7 +12,6 @@ import com.newrelic.api.agent.security.schema.operation.RXSSOperation import com.newrelic.api.agent.security.schema.policy.AgentPolicy import com.newrelic.api.agent.security.utils.logging.LogLevel import fs2.RaiseThrowable -import fs2.text.decodeWithCharset import org.http4s.{Headers, Message, Request, Response} import java.util @@ -94,7 +93,7 @@ object RequestProcessor { private def postProcessSecurityHook[F[_]: Sync](isLockAcquired: Boolean, response: Response[F], body: String): F[Unit] = construct { try { - if (isLockAcquired && NewRelicSecurity.isHookProcessingActive) { + if (isLockAcquired && NewRelicSecurity.isHookProcessingActive && !NewRelicSecurity.getAgent.getIastDetectionCategory.getRxssEnabled) { val securityResponse = NewRelicSecurity.getAgent.getSecurityMetaData.getResponse securityResponse.setResponseCode(response.status.code) processResponseHeaders(response.headers, securityResponse) From acf4326da2332d25816d3b2c6e4ee84955402202 Mon Sep 17 00:00:00 2001 From: idawda Date: Fri, 6 Dec 2024 06:53:43 +0530 Subject: [PATCH 14/14] Update instrumentation unit tests for ember body extraction --- .../ember/server/EmberServerBuilderTest.scala | 44 +++++++++++++------ .../ember/server/EmberServerBuilderTest.scala | 44 +++++++++++++------ 2 files changed, 62 insertions(+), 26 deletions(-) diff --git a/instrumentation-security/http4s-ember-server-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/EmberServerBuilderTest.scala b/instrumentation-security/http4s-ember-server-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/EmberServerBuilderTest.scala index 54664be50..1c289baf7 100644 --- a/instrumentation-security/http4s-ember-server-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/EmberServerBuilderTest.scala +++ b/instrumentation-security/http4s-ember-server-2.12_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/EmberServerBuilderTest.scala @@ -3,14 +3,14 @@ package com.nr.agent.security.instrumentation.ember.server import cats.effect.IO import com.newrelic.agent.security.introspec.{InstrumentationTestConfig, SecurityInstrumentationTestRunner, SecurityIntrospector} import com.newrelic.api.agent.security.instrumentation.helpers.{GenericHelper, ServletHelper} -import com.newrelic.api.agent.security.schema.{SecurityMetaData, VulnerabilityCaseType} import com.newrelic.api.agent.security.schema.operation.RXSSOperation -import org.http4s.HttpRoutes -import org.http4s.Method.GET +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 @@ -26,8 +26,8 @@ class EmberServerBuilderTest { val emberServer = new Http4sTestServer(hostname, port, HttpRoutes.of[IO] { - case GET -> Root / "hello" / name => - Ok(s"Hello, $name.") + case _ -> Root / "hello" / name => + Ok(s"Hello, $name.").map(_.putHeaders(Header.Raw(CIString.apply("content-type"), contentType))) }.orNotFound) @Before @@ -86,10 +86,15 @@ class EmberServerBuilderTest { 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("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 = { @@ -97,23 +102,36 @@ class EmberServerBuilderTest { Assert.assertEquals("Invalid Request content-type.", contentType, metaData.getRequest.getContentType) Assert.assertEquals("Invalid protocol.", "http", metaData.getRequest.getProtocol) - // Assert.assertFalse("response should not be empty", metaData.getResponse.isEmpty) - // Assert.assertEquals("Invalid response content-type.", contentType, metaData.getResponse.getResponseContentType) + 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): Int = { + def makeRequest(url: String, addCSECHeader: Boolean, headerValue: String): Unit = { val u: URL = new URL(url) val conn = u.openConnection.asInstanceOf[HttpURLConnection] - conn.setRequestProperty("content-type", "text/plain; charset=utf-8") + + 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() - conn.getResponseCode + println(conn.getResponseCode) } } diff --git a/instrumentation-security/http4s-ember-server-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/EmberServerBuilderTest.scala b/instrumentation-security/http4s-ember-server-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/EmberServerBuilderTest.scala index 099f8a03d..44e5ef126 100644 --- a/instrumentation-security/http4s-ember-server-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/EmberServerBuilderTest.scala +++ b/instrumentation-security/http4s-ember-server-2.13_0.23/src/test/scala/com/nr/agent/security/instrumentation/ember/server/EmberServerBuilderTest.scala @@ -5,19 +5,19 @@ import com.newrelic.agent.security.introspec.{InstrumentationTestConfig, Securit 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.HttpRoutes -import org.http4s.Method.GET import org.http4s.dsl.io._ import org.http4s.implicits._ +import org.http4s.{Header, HttpRoutes} import org.junit.runner.RunWith import org.junit.{After, Assert, Before, Test} +import org.typelevel.ci.CIString import java.net.{HttpURLConnection, URL} import java.util import java.util.UUID @RunWith(classOf[SecurityInstrumentationTestRunner]) -@InstrumentationTestConfig(includePrefixes = Array("org.http4s", "com.newrelic.agent.security.http4s.ember.server")) +@InstrumentationTestConfig(includePrefixes = Array("org.http4s", "com.newrelic.agent.security.http4s.ember.server", "scala")) class EmberServerBuilderTest { val hostname = "0.0.0.0" @@ -26,8 +26,8 @@ class EmberServerBuilderTest { val emberServer = new Http4sTestServer(hostname, port, HttpRoutes.of[IO] { - case GET -> Root / "hello" / name => - Ok(s"Hello, $name.") + case _ -> Root / "hello" / name => + Ok(s"Hello, $name.").map(_.putHeaders(Header.Raw(CIString.apply("content-type"), contentType))) }.orNotFound) @Before @@ -86,10 +86,15 @@ class EmberServerBuilderTest { 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("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 = { @@ -97,22 +102,35 @@ class EmberServerBuilderTest { Assert.assertEquals("Invalid Request content-type.", contentType, metaData.getRequest.getContentType) Assert.assertEquals("Invalid protocol.", "http", metaData.getRequest.getProtocol) -// Assert.assertFalse("response should not be empty", metaData.getResponse.isEmpty) -// Assert.assertEquals("Invalid response content-type.", contentType, metaData.getResponse.getResponseContentType) + 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): Int = { + def makeRequest(url: String, addCSECHeader: Boolean, headerValue: String): Unit = { val u: URL = new URL(url) val conn = u.openConnection.asInstanceOf[HttpURLConnection] - conn.setRequestProperty("content-type", "text/plain; charset=utf-8") + + 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() - conn.getResponseCode + println(conn.getResponseCode) } }