Skip to content

Commit c9c027f

Browse files
committed
HTTP response schema collection and data classification for spring
Signed-off-by: sezen.leblay <[email protected]>
1 parent 95c0214 commit c9c027f

File tree

4 files changed

+136
-2
lines changed

4 files changed

+136
-2
lines changed

dd-java-agent/instrumentation/spring-webmvc-3.1/src/main/java/datadog/trace/instrumentation/springweb/HttpMessageConverterInstrumentation.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,27 @@ public void methodAdvice(MethodTransformer transformer) {
7171
.and(takesArgument(1, Class.class))
7272
.and(takesArgument(2, named("org.springframework.http.HttpInputMessage"))),
7373
HttpMessageConverterInstrumentation.class.getName() + "$HttpMessageConverterReadAdvice");
74+
75+
transformer.applyAdvice(
76+
isMethod()
77+
.and(isPublic())
78+
.and(named("write"))
79+
.and(takesArguments(3))
80+
.and(takesArgument(0, Object.class))
81+
.and(takesArgument(1, named("org.springframework.http.MediaType")))
82+
.and(takesArgument(2, named("org.springframework.http.HttpOutputMessage"))),
83+
HttpMessageConverterInstrumentation.class.getName() + "$HttpMessageConverterWriteAdvice");
84+
85+
transformer.applyAdvice(
86+
isMethod()
87+
.and(isPublic())
88+
.and(named("write"))
89+
.and(takesArguments(4))
90+
.and(takesArgument(0, Object.class))
91+
.and(takesArgument(1, Type.class))
92+
.and(takesArgument(2, named("org.springframework.http.MediaType")))
93+
.and(takesArgument(3, named("org.springframework.http.HttpOutputMessage"))),
94+
HttpMessageConverterInstrumentation.class.getName() + "$HttpMessageConverterWriteAdvice");
7495
}
7596

7697
@RequiresRequestContext(RequestContextSlot.APPSEC)
@@ -106,4 +127,37 @@ public static void after(
106127
}
107128
}
108129
}
130+
131+
@RequiresRequestContext(RequestContextSlot.APPSEC)
132+
public static class HttpMessageConverterWriteAdvice {
133+
@Advice.OnMethodEnter(suppress = Throwable.class)
134+
public static void before(
135+
@Advice.Argument(0) final Object obj, @ActiveRequestContext RequestContext reqCtx) {
136+
if (obj == null) {
137+
return;
138+
}
139+
140+
CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC);
141+
BiFunction<RequestContext, Object, Flow<Void>> callback =
142+
cbp.getCallback(EVENTS.responseBody());
143+
if (callback == null) {
144+
return;
145+
}
146+
147+
Flow<Void> flow = callback.apply(reqCtx, obj);
148+
Flow.Action action = flow.getAction();
149+
if (action instanceof Flow.Action.RequestBlockingAction) {
150+
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
151+
BlockResponseFunction brf = reqCtx.getBlockResponseFunction();
152+
if (brf != null) {
153+
brf.tryCommitBlockingResponse(
154+
reqCtx.getTraceSegment(),
155+
rba.getStatusCode(),
156+
rba.getBlockingContentType(),
157+
rba.getExtraHeaders());
158+
}
159+
throw new BlockingException("Blocked response (for HttpMessageConverter/write)");
160+
}
161+
}
162+
}
109163
}

dd-java-agent/instrumentation/spring-webmvc-3.1/src/test/groovy/test/boot/SpringBootBasedTest.groovy

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ class SpringBootBasedTest extends HttpServerTest<ConfigurableApplicationContext>
7777
return "boot-context"
7878
}
7979

80+
@Override
81+
boolean testResponseBodyJson() {
82+
return true
83+
}
84+
8085
@Override
8186
String expectedServiceName() {
8287
servletContext
@@ -163,8 +168,7 @@ class SpringBootBasedTest extends HttpServerTest<ConfigurableApplicationContext>
163168

164169
@Override
165170
Map<String, Serializable> expectedExtraServerTags(ServerEndpoint endpoint) {
166-
["servlet.path": endpoint.path, "servlet.context": "/$servletContext"] +
167-
extraServerTags
171+
["servlet.path": endpoint.path, "servlet.context": "/$servletContext"] + extraServerTags
168172
}
169173

170174
@Override

dd-smoke-tests/appsec/springboot/src/main/java/datadog/smoketest/appsec/springboot/controller/WebController.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,13 @@ public ResponseEntity<String> exceedResponseHeaders() {
239239
return new ResponseEntity<>("Custom headers added", headers, HttpStatus.OK);
240240
}
241241

242+
@PostMapping("/api_security/response")
243+
public ResponseEntity<String> apiSecurityResponse(@RequestBody String body) {
244+
// This endpoint is used to test API security response handling
245+
// It simply returns the body received in the request
246+
return ResponseEntity.ok(body);
247+
}
248+
242249
private void withProcess(final Operation<Process> op) {
243250
Process process = null;
244251
try {
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package datadog.smoketest.appsec
2+
3+
import groovy.json.JsonOutput
4+
import okhttp3.MediaType
5+
import okhttp3.Request
6+
import okhttp3.RequestBody
7+
8+
class AppSecHttpMessageConverterSmokeTest extends AbstractAppSecServerSmokeTest {
9+
10+
@Override
11+
def logLevel() {
12+
'DEBUG'
13+
}
14+
15+
@Override
16+
ProcessBuilder createProcessBuilder() {
17+
String springBootShadowJar = System.getProperty("datadog.smoketest.appsec.springboot.shadowJar.path")
18+
19+
List<String> command = new ArrayList<>()
20+
command.add(javaPath())
21+
command.addAll(defaultJavaProperties)
22+
command.addAll(defaultAppSecProperties)
23+
command.addAll((String[]) [
24+
"-Ddd.writer.type=MultiWriter:TraceStructureWriter:${output.getAbsolutePath()},DDAgentWriter",
25+
"-jar",
26+
springBootShadowJar,
27+
"--server.port=${httpPort}"
28+
])
29+
ProcessBuilder processBuilder = new ProcessBuilder(command)
30+
processBuilder.directory(new File(buildDirectory))
31+
}
32+
33+
@Override
34+
File createTemporaryFile() {
35+
return new File("${buildDirectory}/tmp/trace-structure-http-converter.out")
36+
}
37+
38+
void 'test response schema extraction'() {
39+
given:
40+
def url = "http://localhost:${httpPort}/api_security/response"
41+
def body = [
42+
source: 'AppSecSpringSmokeTest',
43+
tests : [
44+
[
45+
name : 'API Security samples only one request per endpoint',
46+
status: 'SUCCESS'
47+
],
48+
[
49+
name : 'test response schema extraction',
50+
status: 'FAILED'
51+
]
52+
]
53+
]
54+
def request = new Request.Builder()
55+
.url(url)
56+
.post(RequestBody.create(MediaType.get('application/json'), JsonOutput.toJson(body)))
57+
.build()
58+
59+
when:
60+
final response = client.newCall(request).execute()
61+
waitForTraceCount(1)
62+
63+
then:
64+
response.code() == 200
65+
def span = rootSpans.first()
66+
span.meta.containsKey('_dd.appsec.s.res.headers')
67+
span.meta.containsKey('_dd.appsec.s.res.body')
68+
}
69+
}

0 commit comments

Comments
 (0)