Skip to content

Commit

Permalink
Merge pull request #228 from newrelic/support/ember-server
Browse files Browse the repository at this point in the history
Http4s-Ember-Server And Client Instrumentation support
  • Loading branch information
IshikaDawda authored Dec 12, 2024
2 parents 804d006 + 9b33298 commit a654667
Show file tree
Hide file tree
Showing 21 changed files with 1,362 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -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 = []
Original file line number Diff line number Diff line change
@@ -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<F> {

private final Async<F> evidence$1 = Weaver.callOriginal();

public Resource<F, Client<F>> build() {
Resource<F, Client<F>> delegateResource = Weaver.callOriginal();
return NewrelicSecurityClientMiddleware$.MODULE$.resource(delegateResource, evidence$1);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package com.newrelic.agent.security.instrumentation.http4s.ember

import cats.effect.kernel.Async
import cats.effect.{Resource, Sync}
import com.newrelic.api.agent.security.NewRelicSecurity
import com.newrelic.api.agent.security.instrumentation.helpers.{GenericHelper, ServletHelper}
import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityException
import com.newrelic.api.agent.security.schema.operation.SSRFOperation
import com.newrelic.api.agent.security.schema.{AbstractOperation, StringUtils, VulnerabilityCaseType}
import com.newrelic.api.agent.security.utils.SSRFUtils
import com.newrelic.api.agent.security.utils.logging.LogLevel
import org.http4s.Request
import org.http4s.client.Client

import java.net.URI

object NewrelicSecurityClientMiddleware {
private final val nrSecCustomAttrName: String = "HTTP4S-EMBER-CLIENT-OUTBOUND"
private final val HTTP4S_EMBER_CLIENT: String = "HTTP4S-EMBER-CLIENT-2.12_0.23"

private def construct[F[_] : Sync, T](t: T): F[T] = Sync[F].delay(t)

private def clientResource[F[_] : Async](client: Client[F]): Client[F] =
Client { req: Request[F] =>
for {
// pre-process hook
operation <- Resource.eval(
construct {
val isLockAcquired = GenericHelper.acquireLockIfPossible(VulnerabilityCaseType.HTTP_REQUEST, nrSecCustomAttrName)
var operation: AbstractOperation = null
if (isLockAcquired) {
operation = preprocessSecurityHook(req)
}
operation
})
// add Security Headers
request <- Resource.eval(construct {addSecurityHeaders(req, operation)})

// original call
response <- client.run(request)

// post process and register exit event
newRes <- Resource.eval(construct{
val isLockAcquired = GenericHelper.isLockAcquired(nrSecCustomAttrName);
if (isLockAcquired) {
GenericHelper.releaseLock(nrSecCustomAttrName)
}
registerExitOperation(isLockAcquired, operation)
response
})

} yield newRes
}

def resource[F[_] : Async](delegate: Resource[F, Client[F]]): Resource[F, Client[F]] = {
val res: Resource[F, Client[F]] = delegate.map(c =>clientResource(c))
res
}


private def preprocessSecurityHook[F[_] : Async](httpRequest: Request[F]): AbstractOperation = {
try {
val securityMetaData = NewRelicSecurity.getAgent.getSecurityMetaData
if (!NewRelicSecurity.isHookProcessingActive || securityMetaData.getRequest.isEmpty) return null
// Generate required URL
var methodURI: URI = null
var uri: String = null
try {
methodURI = new URI(httpRequest.uri.toString)
uri = methodURI.toString
if (methodURI == null) return null
} catch {
case ignored: Exception =>
NewRelicSecurity.getAgent.log(LogLevel.WARNING, String.format(GenericHelper.URI_EXCEPTION_MESSAGE, HTTP4S_EMBER_CLIENT, ignored.getMessage), ignored, this.getClass.getName)
return null
}
return new SSRFOperation(uri, this.getClass.getName, "run")
} catch {
case e: Throwable =>
if (e.isInstanceOf[NewRelicSecurityException]) {
NewRelicSecurity.getAgent.log(LogLevel.WARNING, String.format(GenericHelper.SECURITY_EXCEPTION_MESSAGE, HTTP4S_EMBER_CLIENT, e.getMessage), e, this.getClass.getName)
throw e
}
NewRelicSecurity.getAgent.log(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP4S_EMBER_CLIENT, e.getMessage), e, this.getClass.getName)
NewRelicSecurity.getAgent.reportIncident(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP4S_EMBER_CLIENT, e.getMessage), e, this.getClass.getName)
}
null
}

private def addSecurityHeaders[F[_] : Async](request: Request[F], operation: AbstractOperation): Request[F] = {
val outboundRequest = new OutboundRequest(request)
if (operation != null) {
val securityMetaData = NewRelicSecurity.getAgent.getSecurityMetaData
val iastHeader = NewRelicSecurity.getAgent.getSecurityMetaData.getFuzzRequestIdentifier.getRaw
if (iastHeader != null && !iastHeader.trim.isEmpty) {
outboundRequest.setHeader(ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID, iastHeader)
}
val csecParentId = securityMetaData.getCustomAttribute(GenericHelper.CSEC_PARENT_ID, classOf[String])
if (StringUtils.isNotBlank(csecParentId)) {
outboundRequest.setHeader(GenericHelper.CSEC_PARENT_ID, csecParentId)
}
try {
NewRelicSecurity.getAgent.getSecurityMetaData.getMetaData.setFromJumpRequiredInStackTrace(Integer.valueOf(4))
NewRelicSecurity.getAgent.registerOperation(operation)
}
finally {
if (operation.getApiID != null && !operation.getApiID.trim.isEmpty && operation.getExecutionId != null && !operation.getExecutionId.trim.isEmpty) {
outboundRequest.setHeader(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER, SSRFUtils.generateTracingHeaderValue(securityMetaData.getTracingHeaderValue, operation.getApiID, operation.getExecutionId, NewRelicSecurity.getAgent.getAgentUUID))
}
}
}
outboundRequest.getRequest
}

private def registerExitOperation(isProcessingAllowed: Boolean, operation: AbstractOperation): Unit = {
try {
if (operation == null || !isProcessingAllowed || !NewRelicSecurity.isHookProcessingActive || NewRelicSecurity.getAgent.getSecurityMetaData.getRequest.isEmpty) return
NewRelicSecurity.getAgent.registerExitEvent(operation)
} catch {
case e: Throwable =>
NewRelicSecurity.getAgent.log(LogLevel.FINEST, String.format(GenericHelper.EXIT_OPERATION_EXCEPTION_MESSAGE, HTTP4S_EMBER_CLIENT, e.getMessage), e, this.getClass.getName)
}
}
}

Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
}

Original file line number Diff line number Diff line change
@@ -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 = []
Original file line number Diff line number Diff line change
@@ -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<F> {

private final Async<F> evidence$1 = Weaver.callOriginal();
public Resource<F, Client<F>> build() {
Resource<F, Client<F>> delegateResource = Weaver.callOriginal();
return NewrelicSecurityClientMiddleware$.MODULE$.resource(delegateResource, evidence$1);
}
}
Loading

0 comments on commit a654667

Please sign in to comment.