Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Jersey support
  • Loading branch information
jandro996 committed Jun 26, 2025
commit db61f58eaf6223fff8927532982ccd95d4a2412a
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@
0 com.fasterxml.jackson.databind.util.TokenBuffer$Parser
0 com.fasterxml.jackson.databind.ObjectMapper
0 com.fasterxml.jackson.module.afterburner.util.MyClassLoader
# Included for API Security response schema collection
0 com.fasterxml.jackson.jaxrs.*
2 com.github.mustachejava.*
2 com.google.api.*
0 com.google.api.client.http.HttpRequest
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package datadog.trace.instrumentation.jakarta3;

import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.implementsInterface;
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
import static datadog.trace.api.gateway.Events.EVENTS;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;

import com.google.auto.service.AutoService;
import datadog.appsec.api.blocking.BlockingException;
import datadog.trace.advice.ActiveRequestContext;
import datadog.trace.advice.RequiresRequestContext;
import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.agent.tooling.InstrumenterModule;
import datadog.trace.api.gateway.BlockResponseFunction;
import datadog.trace.api.gateway.CallbackProvider;
import datadog.trace.api.gateway.Flow;
import datadog.trace.api.gateway.RequestContext;
import datadog.trace.api.gateway.RequestContextSlot;
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
import jakarta.ws.rs.core.MediaType;
import java.util.function.BiFunction;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;

@AutoService(InstrumenterModule.class)
public class MessageBodyWriterInstrumentation extends InstrumenterModule.AppSec
implements Instrumenter.ForTypeHierarchy, Instrumenter.HasMethodAdvice {

public MessageBodyWriterInstrumentation() {
super("jakarta-rs");
}

@Override
public String hierarchyMarkerType() {
return "jakarta.ws.rs.ext.MessageBodyWriter";
}

@Override
public ElementMatcher<TypeDescription> hierarchyMatcher() {
return implementsInterface(named(hierarchyMarkerType()));
}

@Override
public void methodAdvice(MethodTransformer transformer) {
transformer.applyAdvice(
named("writeTo").and(takesArguments(7)), getClass().getName() + "$MessageBodyWriterAdvice");
}

@RequiresRequestContext(RequestContextSlot.APPSEC)
public static class MessageBodyWriterAdvice {
@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
static void after(
@Advice.Argument(0) Object entity,
@Advice.Argument(4) MediaType mediaType,
@ActiveRequestContext RequestContext reqCtx,
@Advice.Thrown Throwable t) {
if (t != null) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should be building method enter advices this cases, so we don´t write the response to the output (in case it needs to be blocked),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok!

return;
}

// TODO check if this works or is better to use JSON MediaType
if (!MediaType.APPLICATION_JSON_TYPE.isCompatible(mediaType)) {
return;
}

CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC);
BiFunction<RequestContext, Object, Flow<Void>> callback =
cbp.getCallback(EVENTS.responseBody());
if (callback == null) {
return;
}

Flow<Void> flow = callback.apply(reqCtx, entity);
Flow.Action action = flow.getAction();
if (action instanceof Flow.Action.RequestBlockingAction) {
BlockResponseFunction blockResponseFunction = reqCtx.getBlockResponseFunction();
if (blockResponseFunction == null) {
return;
}
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
blockResponseFunction.tryCommitBlockingResponse(
reqCtx.getTraceSegment(),
rba.getStatusCode(),
rba.getBlockingContentType(),
rba.getExtraHeaders());

throw new BlockingException("Blocked request (for MessageBodyWriter)");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ muzzle {
module = "javax.ws.rs-api"
versions = "[,]"
}
pass {
group = "javax.ws.rs"
module = "javax.ws.rs-api"
name = 'javax-message-body-writer'
versions = "[,]"
}
}

apply from: "$rootDir/gradle/java.gradle"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package datadog.trace.instrumentation.jaxrs2;

import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.implementsInterface;
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
import static datadog.trace.api.gateway.Events.EVENTS;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;

import com.google.auto.service.AutoService;
import datadog.appsec.api.blocking.BlockingException;
import datadog.trace.advice.ActiveRequestContext;
import datadog.trace.advice.RequiresRequestContext;
import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.agent.tooling.InstrumenterModule;
import datadog.trace.api.gateway.BlockResponseFunction;
import datadog.trace.api.gateway.CallbackProvider;
import datadog.trace.api.gateway.Flow;
import datadog.trace.api.gateway.RequestContext;
import datadog.trace.api.gateway.RequestContextSlot;
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
import java.util.function.BiFunction;
import javax.ws.rs.core.MediaType;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;

@AutoService(InstrumenterModule.class)
public class MessageBodyWriterInstrumentation extends InstrumenterModule.AppSec
implements Instrumenter.ForTypeHierarchy, Instrumenter.HasMethodAdvice {

public MessageBodyWriterInstrumentation() {
super("jax-rs");
}

@Override
public String muzzleDirective() {
return "javax-message-body-writer";
}

@Override
public String hierarchyMarkerType() {
return "javax.ws.rs.ext.MessageBodyWriter";
}

@Override
public ElementMatcher<TypeDescription> hierarchyMatcher() {
return implementsInterface(named(hierarchyMarkerType()));
}

@Override
public void methodAdvice(MethodTransformer transformer) {
transformer.applyAdvice(
named("writeTo").and(takesArguments(7)), getClass().getName() + "$MessageBodyWriterAdvice");
}

@RequiresRequestContext(RequestContextSlot.APPSEC)
public static class MessageBodyWriterAdvice {
@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
static void after(
@Advice.Argument(0) Object entity,
@Advice.Argument(4) MediaType mediaType,
@ActiveRequestContext RequestContext reqCtx,
@Advice.Thrown Throwable t) {
if (t != null) {
return;
}

// TODO check if this works or is better to use JSON MediaType
if (!MediaType.APPLICATION_JSON_TYPE.isCompatible(mediaType)) {
return;
}

CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC);
BiFunction<RequestContext, Object, Flow<Void>> callback =
cbp.getCallback(EVENTS.responseBody());
if (callback == null) {
return;
}

Flow<Void> flow = callback.apply(reqCtx, entity);
Flow.Action action = flow.getAction();
if (action instanceof Flow.Action.RequestBlockingAction) {
BlockResponseFunction blockResponseFunction = reqCtx.getBlockResponseFunction();
if (blockResponseFunction == null) {
return;
}
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
blockResponseFunction.tryCommitBlockingResponse(
reqCtx.getTraceSegment(),
rba.getStatusCode(),
rba.getBlockingContentType(),
rba.getExtraHeaders());

throw new BlockingException("Blocked request (for MessageBodyWriter)");
}
}
}
}
2 changes: 2 additions & 0 deletions dd-java-agent/instrumentation/jersey/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ dependencies {
jersey2JettyTestRuntimeOnly group: 'javax.xml.bind', name: 'jaxb-api', version: '2.2.3'
jersey2JettyTestRuntimeOnly project(':dd-java-agent:instrumentation:jetty-9')
jersey2JettyTestRuntimeOnly project(':dd-java-agent:instrumentation:jersey-2-appsec')
jersey2JettyTestRuntimeOnly project(':dd-java-agent:instrumentation:jax-rs-annotations-2')

jersey3JettyTestImplementation project(':dd-java-agent:testing'), {
exclude group: 'org.eclipse.jetty', module: 'jetty-server'
Expand All @@ -72,6 +73,7 @@ dependencies {
jersey3JettyTestRuntimeOnly project(':dd-java-agent:instrumentation:jetty-11')
jersey3JettyTestRuntimeOnly project(':dd-java-agent:instrumentation:jersey-2-appsec')
jersey3JettyTestRuntimeOnly project(':dd-java-agent:instrumentation:jersey-3-appsec')
jersey3JettyTestRuntimeOnly project(':dd-java-agent:instrumentation:jakarta-rs-annotations-3')
}

configurations.getByName('jersey3JettyTestRuntimeClasspath').resolutionStrategy {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
package datadog.trace.instrumentation.jersey2

import groovy.json.JsonBuilder

class ClassToConvertBodyTo {
String a

@Override
String toString() {
new JsonBuilder([a: a]).toString()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import javax.ws.rs.ext.ExceptionMapper

class Jersey2JettyTest extends HttpServerTest<JettyServer> {

@Override
boolean testResponseBodyJson() {
return true
}

@Override
HttpServer server() {
new JettyServer()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import javax.ws.rs.HeaderParam
import javax.ws.rs.POST
import javax.ws.rs.Path
import javax.ws.rs.PathParam
import javax.ws.rs.Produces
import javax.ws.rs.QueryParam
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.Response
Expand Down Expand Up @@ -87,10 +88,14 @@ class ServiceResource {

@POST
@Path("body-json")
@Produces(MediaType.APPLICATION_JSON)
Response bodyJson(ClassToConvertBodyTo obj) {
controller(BODY_JSON) {
Response.status(BODY_JSON.status).entity("""{"a":"${obj.a}"}""" as String).build()
}
return controller(BODY_JSON, () -> {
Response response = Response.status(BODY_JSON.status)
.entity(obj)
.build()
return response
})
}

@GET
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
package datadog.trace.instrumentation.jersey3

import groovy.json.JsonBuilder

class ClassToConvertBodyTo {
String a

@Override
String toString() {
new JsonBuilder([a: a]).toString()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import jakarta.ws.rs.ext.ExceptionMapper

class Jersey3JettyTest extends HttpServerTest<JettyServer> {

@Override
boolean testResponseBodyJson() {
return true
}

@Override
HttpServer server() {
new JettyServer()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package datadog.trace.instrumentation.jersey3

import datadog.appsec.api.blocking.Blocking
import jakarta.ws.rs.Produces
import org.glassfish.jersey.media.multipart.FormDataParam

import jakarta.ws.rs.Consumes
Expand Down Expand Up @@ -87,10 +88,13 @@ class ServiceResource {

@POST
@Path("body-json")
@Produces(MediaType.APPLICATION_JSON)
Response bodyJson(ClassToConvertBodyTo obj) {
controller(BODY_JSON) {
Response.status(BODY_JSON.status).entity("""{"a":"${obj.a}"}""" as String).build()
}
controller(BODY_JSON, () ->
Response.status(BODY_JSON.status)
.entity(obj)
.build()
)
}

@GET
Expand Down
1 change: 1 addition & 0 deletions dd-smoke-tests/jersey-2/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dependencies {
implementation group: 'javax.xml', name: 'jaxb-api', version:'2.1'
testImplementation project(':dd-smoke-tests')
testImplementation(testFixtures(project(":dd-smoke-tests:iast-util")))
testImplementation project(':dd-smoke-tests:appsec')
}

tasks.withType(Test).configureEach {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,18 @@ public Response responseLocation(@QueryParam("param") String param) throws URISy
public Response getCookie() throws SQLException {
return Response.ok().cookie(new NewCookie("user-id", "7")).build();
}

@Path("/api_security/response")
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response bodyJson() {
TestEntity testEntity = new TestEntity("testing", "test");
return Response.ok().entity(testEntity).build();
}

@GET
@Path("/api_security/sampling/{i}")
public Response apiSecuritySamplingWithStatus(@PathParam("i") int i) {
return Response.status(i).header("content-type", "text/plain").entity("Hello!\n").build();
}
}
Loading