Skip to content

Commit

Permalink
NR-325526: Instrument outgoing HTTP Request in HTTP4s Blaze client
Browse files Browse the repository at this point in the history
  • Loading branch information
IshikaDawda committed Oct 29, 2024
1 parent d239f0f commit e9b7015
Show file tree
Hide file tree
Showing 19 changed files with 882 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
apply plugin: 'scala'

isScalaProjectEnabled(project, "scala-2.12")

dependencies {
implementation(project(":newrelic-security-api"))
implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}")
implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}")
implementation("org.scala-lang:scala-library:2.12.14")
implementation('org.http4s:http4s-blaze-client_2.12:0.21.24')
implementation("org.typelevel:cats-effect_2.12:2.5.5")
}

jar {
manifest {
attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.http4s-blaze-client-2.12_0.21', 'Priority': '-1'
}
}

verifyInstrumentation {
passes 'org.http4s:http4s-blaze-client_2.12:[0.21,0.22)'
excludeRegex '.*(RC|M)[0-9]*'
}

sourceSets.main.scala.srcDirs = ['src/main/scala', 'src/main/java']
sourceSets.main.java.srcDirs = []
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.http4s;

import cats.effect.ConcurrentEffect;
import cats.effect.Resource;
import com.newrelic.agent.security.instrumentation.http4s.blaze.NewrelicSecurityClientMiddleware$;
import com.newrelic.api.agent.weaver.MatchType;
import com.newrelic.api.agent.weaver.Weave;
import com.newrelic.api.agent.weaver.Weaver;
import org.http4s.client.Client;

@Weave(type = MatchType.ExactClass, originalName = "org.http4s.client.blaze.BlazeClientBuilder")
public abstract class BlazeClientBuilder_Instrumentation<F> {

public ConcurrentEffect F() {
return Weaver.callOriginal();
}

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

import cats.effect.{Async, ConcurrentEffect, Resource, Sync}
import com.newrelic.api.agent.security.NewRelicSecurity
import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper
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-BLAZE-CLIENT-OUTBOUND"
private final val HTTP4S_BLAZE_CLIENT: String = "HTTP4S-BLAZE-CLIENT-2.12_0.21"

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

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

// TODO add Security Headers

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

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

} yield newRes
}

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


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

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

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-blaze-client_2.12:0.22.14')
implementation("org.typelevel:cats-effect_2.12:2.5.5")
}

jar {
manifest {
attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.http4s-blaze-client-2.12_0.22', 'Priority': '-1'
}
}

verifyInstrumentation {
passes 'org.http4s:http4s-blaze-client_2.12:[0.22.0,0.23.0)'
excludeRegex '.*(RC|M)[0-9]*'
excludeRegex '.*0.22\\-[0-9].*'
}

sourceSets.main.scala.srcDirs = ['src/main/scala', 'src/main/java']
sourceSets.main.java.srcDirs = []
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.http4s;

import cats.effect.ConcurrentEffect;
import cats.effect.Resource;
import com.newrelic.agent.security.instrumentation.http4s.blaze.NewrelicSecurityClientMiddleware$;
import com.newrelic.api.agent.weaver.MatchType;
import com.newrelic.api.agent.weaver.Weave;
import com.newrelic.api.agent.weaver.Weaver;
import org.http4s.client.Client;

@Weave(type = MatchType.ExactClass, originalName = "org.http4s.blaze.client.BlazeClientBuilder")
public abstract class BlazeClientBuilder_Instrumentation<F> {

public ConcurrentEffect F() {
return Weaver.callOriginal();
}

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

import cats.effect.{Async, Resource, Sync}
import com.newrelic.api.agent.security.NewRelicSecurity
import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper
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-BLAZE-CLIENT-OUTBOUND"
private final val HTTP4S_BLAZE_CLIENT: String = "HTTP4S-BLAZE-CLIENT-2.12_0.22"

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

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

// 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_BLAZE_CLIENT, ignored.getMessage), ignored, this.getClass.getName)
return null
}
return new SSRFOperation(uri, this.getClass.getName, "run")
} catch {
case e: Throwable =>
if (e.isInstanceOf[NewRelicSecurityException]) {
NewRelicSecurity.getAgent.log(LogLevel.WARNING, String.format(GenericHelper.SECURITY_EXCEPTION_MESSAGE, HTTP4S_BLAZE_CLIENT, e.getMessage), e, this.getClass.getName)
throw e
}
NewRelicSecurity.getAgent.log(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP4S_BLAZE_CLIENT, e.getMessage), e, this.getClass.getName)
NewRelicSecurity.getAgent.reportIncident(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP4S_BLAZE_CLIENT, e.getMessage), e, this.getClass.getName)
}
null
}

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
apply plugin: 'scala'

isScalaProjectEnabled(project, "scala-2.12")

dependencies {
implementation(project(":newrelic-security-api"))
implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}")
implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}")
implementation("org.scala-lang:scala-library:2.12.14")
implementation('org.http4s:http4s-blaze-client_2.12:0.23.12')
implementation("org.typelevel:cats-effect_2.12:3.3.12")
}

jar {
manifest {
attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.http4s-blaze-client-2.12_0.23', 'Priority': '-1'
}
}
verifyInstrumentation {
passes 'org.http4s:http4s-blaze-client_2.12:[0.23.0,0.24.0)'
excludeRegex '.*(RC|M)[0-9]*'
}

sourceSets.main.scala.srcDirs = ['src/main/scala', 'src/main/java']
sourceSets.main.java.srcDirs = []
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.http4s;

import cats.effect.kernel.Async;
import cats.effect.kernel.Resource;
import com.newrelic.agent.security.instrumentation.http4s.blaze.NewrelicSecurityClientMiddleware$;
import com.newrelic.api.agent.weaver.MatchType;
import com.newrelic.api.agent.weaver.Weave;
import com.newrelic.api.agent.weaver.Weaver;
import org.http4s.client.Client;

@Weave(type = MatchType.ExactClass, originalName = "org.http4s.blaze.client.BlazeClientBuilder")
public abstract class BlazeClientBuilder_Instrumentation<F> {

public Async F() {
return Weaver.callOriginal();
}

public Resource<F, Client<F>> resource() {
Resource<F, Client<F>> delegateResource = Weaver.callOriginal();
return NewrelicSecurityClientMiddleware$.MODULE$.resource(delegateResource, F());
}
}
Loading

0 comments on commit e9b7015

Please sign in to comment.