Skip to content

Commit 46d0949

Browse files
authored
Extract Jersey json body response schemas (#9014)
What Does This Do Adds response body extraction for Jersey JSON endpoints to enable automatic API schema discovery and protection by the Web Application Firewall (WAF). Jira ticket: APPSEC-57909
1 parent 534111d commit 46d0949

File tree

21 files changed

+547
-8
lines changed

21 files changed

+547
-8
lines changed

dd-java-agent/agent-tooling/src/main/resources/datadog/trace/agent/tooling/bytebuddy/matcher/ignored_class_name.trie

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@
190190
0 com.fasterxml.jackson.databind.util.TokenBuffer$Parser
191191
0 com.fasterxml.jackson.databind.ObjectMapper
192192
0 com.fasterxml.jackson.module.afterburner.util.MyClassLoader
193+
# Included for API Security response schema collection
194+
0 com.fasterxml.jackson.jaxrs.*
193195
2 com.github.mustachejava.*
194196
2 com.google.api.*
195197
0 com.google.api.client.http.HttpRequest
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package datadog.trace.instrumentation.jakarta3;
2+
3+
import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.implementsInterface;
4+
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
5+
import static datadog.trace.api.gateway.Events.EVENTS;
6+
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
7+
8+
import com.google.auto.service.AutoService;
9+
import datadog.appsec.api.blocking.BlockingException;
10+
import datadog.trace.advice.ActiveRequestContext;
11+
import datadog.trace.advice.RequiresRequestContext;
12+
import datadog.trace.agent.tooling.Instrumenter;
13+
import datadog.trace.agent.tooling.InstrumenterModule;
14+
import datadog.trace.api.gateway.BlockResponseFunction;
15+
import datadog.trace.api.gateway.CallbackProvider;
16+
import datadog.trace.api.gateway.Flow;
17+
import datadog.trace.api.gateway.RequestContext;
18+
import datadog.trace.api.gateway.RequestContextSlot;
19+
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
20+
import jakarta.ws.rs.core.MediaType;
21+
import java.util.function.BiFunction;
22+
import net.bytebuddy.asm.Advice;
23+
import net.bytebuddy.description.type.TypeDescription;
24+
import net.bytebuddy.matcher.ElementMatcher;
25+
26+
@AutoService(InstrumenterModule.class)
27+
public class MessageBodyWriterInstrumentation extends InstrumenterModule.AppSec
28+
implements Instrumenter.ForTypeHierarchy, Instrumenter.HasMethodAdvice {
29+
30+
public MessageBodyWriterInstrumentation() {
31+
super("jakarta-rs");
32+
}
33+
34+
@Override
35+
public String hierarchyMarkerType() {
36+
return "jakarta.ws.rs.ext.MessageBodyWriter";
37+
}
38+
39+
@Override
40+
public ElementMatcher<TypeDescription> hierarchyMatcher() {
41+
return implementsInterface(named(hierarchyMarkerType()));
42+
}
43+
44+
@Override
45+
public void methodAdvice(MethodTransformer transformer) {
46+
transformer.applyAdvice(
47+
named("writeTo").and(takesArguments(7)), getClass().getName() + "$MessageBodyWriterAdvice");
48+
}
49+
50+
@RequiresRequestContext(RequestContextSlot.APPSEC)
51+
public static class MessageBodyWriterAdvice {
52+
@Advice.OnMethodEnter(suppress = Throwable.class)
53+
static void before(
54+
@Advice.Argument(0) Object entity,
55+
@Advice.Argument(4) MediaType mediaType,
56+
@ActiveRequestContext RequestContext reqCtx) {
57+
58+
if (!MediaType.APPLICATION_JSON_TYPE.isCompatible(mediaType)) {
59+
return;
60+
}
61+
62+
CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC);
63+
BiFunction<RequestContext, Object, Flow<Void>> callback =
64+
cbp.getCallback(EVENTS.responseBody());
65+
if (callback == null) {
66+
return;
67+
}
68+
69+
Flow<Void> flow = callback.apply(reqCtx, entity);
70+
Flow.Action action = flow.getAction();
71+
if (action instanceof Flow.Action.RequestBlockingAction) {
72+
BlockResponseFunction blockResponseFunction = reqCtx.getBlockResponseFunction();
73+
if (blockResponseFunction == null) {
74+
return;
75+
}
76+
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
77+
blockResponseFunction.tryCommitBlockingResponse(
78+
reqCtx.getTraceSegment(),
79+
rba.getStatusCode(),
80+
rba.getBlockingContentType(),
81+
rba.getExtraHeaders());
82+
83+
throw new BlockingException("Blocked request (for MessageBodyWriter)");
84+
}
85+
}
86+
}
87+
}

dd-java-agent/instrumentation/jax-rs-annotations-2/build.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ muzzle {
1010
module = "javax.ws.rs-api"
1111
versions = "[,]"
1212
}
13+
pass {
14+
group = "javax.ws.rs"
15+
module = "javax.ws.rs-api"
16+
name = 'javax-message-body-writer'
17+
versions = "[,]"
18+
}
1319
}
1420

1521
apply from: "$rootDir/gradle/java.gradle"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package datadog.trace.instrumentation.jaxrs2;
2+
3+
import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.implementsInterface;
4+
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
5+
import static datadog.trace.api.gateway.Events.EVENTS;
6+
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
7+
8+
import com.google.auto.service.AutoService;
9+
import datadog.appsec.api.blocking.BlockingException;
10+
import datadog.trace.advice.ActiveRequestContext;
11+
import datadog.trace.advice.RequiresRequestContext;
12+
import datadog.trace.agent.tooling.Instrumenter;
13+
import datadog.trace.agent.tooling.InstrumenterModule;
14+
import datadog.trace.api.gateway.BlockResponseFunction;
15+
import datadog.trace.api.gateway.CallbackProvider;
16+
import datadog.trace.api.gateway.Flow;
17+
import datadog.trace.api.gateway.RequestContext;
18+
import datadog.trace.api.gateway.RequestContextSlot;
19+
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
20+
import java.util.function.BiFunction;
21+
import javax.ws.rs.core.MediaType;
22+
import net.bytebuddy.asm.Advice;
23+
import net.bytebuddy.description.type.TypeDescription;
24+
import net.bytebuddy.matcher.ElementMatcher;
25+
26+
@AutoService(InstrumenterModule.class)
27+
public class MessageBodyWriterInstrumentation extends InstrumenterModule.AppSec
28+
implements Instrumenter.ForTypeHierarchy, Instrumenter.HasMethodAdvice {
29+
30+
public MessageBodyWriterInstrumentation() {
31+
super("jax-rs");
32+
}
33+
34+
@Override
35+
public String muzzleDirective() {
36+
return "javax-message-body-writer";
37+
}
38+
39+
@Override
40+
public String hierarchyMarkerType() {
41+
return "javax.ws.rs.ext.MessageBodyWriter";
42+
}
43+
44+
@Override
45+
public ElementMatcher<TypeDescription> hierarchyMatcher() {
46+
return implementsInterface(named(hierarchyMarkerType()));
47+
}
48+
49+
@Override
50+
public void methodAdvice(MethodTransformer transformer) {
51+
transformer.applyAdvice(
52+
named("writeTo").and(takesArguments(7)), getClass().getName() + "$MessageBodyWriterAdvice");
53+
}
54+
55+
@RequiresRequestContext(RequestContextSlot.APPSEC)
56+
public static class MessageBodyWriterAdvice {
57+
@Advice.OnMethodEnter(suppress = Throwable.class)
58+
static void before(
59+
@Advice.Argument(0) Object entity,
60+
@Advice.Argument(4) MediaType mediaType,
61+
@ActiveRequestContext RequestContext reqCtx) {
62+
63+
if (!MediaType.APPLICATION_JSON_TYPE.isCompatible(mediaType)) {
64+
return;
65+
}
66+
67+
CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC);
68+
BiFunction<RequestContext, Object, Flow<Void>> callback =
69+
cbp.getCallback(EVENTS.responseBody());
70+
if (callback == null) {
71+
return;
72+
}
73+
74+
Flow<Void> flow = callback.apply(reqCtx, entity);
75+
Flow.Action action = flow.getAction();
76+
if (action instanceof Flow.Action.RequestBlockingAction) {
77+
BlockResponseFunction blockResponseFunction = reqCtx.getBlockResponseFunction();
78+
if (blockResponseFunction == null) {
79+
return;
80+
}
81+
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
82+
blockResponseFunction.tryCommitBlockingResponse(
83+
reqCtx.getTraceSegment(),
84+
rba.getStatusCode(),
85+
rba.getBlockingContentType(),
86+
rba.getExtraHeaders());
87+
88+
throw new BlockingException("Blocked request (for MessageBodyWriter)");
89+
}
90+
}
91+
}
92+
}

dd-java-agent/instrumentation/jersey/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ dependencies {
5656
jersey2JettyTestRuntimeOnly group: 'javax.xml.bind', name: 'jaxb-api', version: '2.2.3'
5757
jersey2JettyTestRuntimeOnly project(':dd-java-agent:instrumentation:jetty-9')
5858
jersey2JettyTestRuntimeOnly project(':dd-java-agent:instrumentation:jersey-2-appsec')
59+
jersey2JettyTestRuntimeOnly project(':dd-java-agent:instrumentation:jax-rs-annotations-2')
5960

6061
jersey3JettyTestImplementation project(':dd-java-agent:testing'), {
6162
exclude group: 'org.eclipse.jetty', module: 'jetty-server'
@@ -72,6 +73,7 @@ dependencies {
7273
jersey3JettyTestRuntimeOnly project(':dd-java-agent:instrumentation:jetty-11')
7374
jersey3JettyTestRuntimeOnly project(':dd-java-agent:instrumentation:jersey-2-appsec')
7475
jersey3JettyTestRuntimeOnly project(':dd-java-agent:instrumentation:jersey-3-appsec')
76+
jersey3JettyTestRuntimeOnly project(':dd-java-agent:instrumentation:jakarta-rs-annotations-3')
7577
}
7678

7779
configurations.getByName('jersey3JettyTestRuntimeClasspath').resolutionStrategy {
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
package datadog.trace.instrumentation.jersey2
22

3+
import groovy.json.JsonBuilder
4+
35
class ClassToConvertBodyTo {
46
String a
7+
8+
@Override
9+
String toString() {
10+
new JsonBuilder([a: a]).toString()
11+
}
512
}

dd-java-agent/instrumentation/jersey/src/jersey2JettyTest/groovy/datadog/trace/instrumentation/jersey2/Jersey2JettyTest.groovy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ import javax.ws.rs.ext.ExceptionMapper
99

1010
class Jersey2JettyTest extends HttpServerTest<JettyServer> {
1111

12+
@Override
13+
boolean testResponseBodyJson() {
14+
return true
15+
}
16+
1217
@Override
1318
HttpServer server() {
1419
new JettyServer()

dd-java-agent/instrumentation/jersey/src/jersey2JettyTest/groovy/datadog/trace/instrumentation/jersey2/ServiceResource.groovy

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import javax.ws.rs.HeaderParam
1010
import javax.ws.rs.POST
1111
import javax.ws.rs.Path
1212
import javax.ws.rs.PathParam
13+
import javax.ws.rs.Produces
1314
import javax.ws.rs.QueryParam
1415
import javax.ws.rs.core.MediaType
1516
import javax.ws.rs.core.Response
@@ -87,10 +88,14 @@ class ServiceResource {
8788

8889
@POST
8990
@Path("body-json")
91+
@Produces(MediaType.APPLICATION_JSON)
9092
Response bodyJson(ClassToConvertBodyTo obj) {
91-
controller(BODY_JSON) {
92-
Response.status(BODY_JSON.status).entity("""{"a":"${obj.a}"}""" as String).build()
93-
}
93+
return controller(BODY_JSON, () -> {
94+
Response response = Response.status(BODY_JSON.status)
95+
.entity(obj)
96+
.build()
97+
return response
98+
})
9499
}
95100

96101
@GET
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
package datadog.trace.instrumentation.jersey3
22

3+
import groovy.json.JsonBuilder
4+
35
class ClassToConvertBodyTo {
46
String a
7+
8+
@Override
9+
String toString() {
10+
new JsonBuilder([a: a]).toString()
11+
}
512
}

dd-java-agent/instrumentation/jersey/src/jersey3JettyTest/groovy/datadog/trace/instrumentation/jersey3/Jersey3JettyTest.groovy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import jakarta.ws.rs.ext.ExceptionMapper
88

99
class Jersey3JettyTest extends HttpServerTest<JettyServer> {
1010

11+
@Override
12+
boolean testResponseBodyJson() {
13+
return true
14+
}
15+
1116
@Override
1217
HttpServer server() {
1318
new JettyServer()

0 commit comments

Comments
 (0)