Skip to content

Commit 5dcd1cf

Browse files
committed
Collect parsed request body if RASP event
1 parent c117ce0 commit 5dcd1cf

File tree

7 files changed

+167
-4
lines changed

7 files changed

+167
-4
lines changed

dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WAFModule.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@ public void onDataAvailable(
478478
}
479479

480480
if (gwCtx.isRasp) {
481+
reqCtx.setRaspMatched(true);
481482
WafMetricCollector.get().raspRuleMatch(gwCtx.raspRuleType);
482483
}
483484

dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/AppSecRequestContext.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ public class AppSecRequestContext implements DataBundle, Closeable {
129129
private volatile int wafTimeouts;
130130
private volatile int raspTimeouts;
131131

132+
private volatile Object processedRequestBody;
133+
private volatile boolean raspMatched;
134+
132135
// keep a reference to the last published usr.id
133136
private volatile String userId;
134137
// keep a reference to the last published usr.login
@@ -145,7 +148,7 @@ public class AppSecRequestContext implements DataBundle, Closeable {
145148
private static final AtomicIntegerFieldUpdater<AppSecRequestContext> WAF_TIMEOUTS_UPDATER =
146149
AtomicIntegerFieldUpdater.newUpdater(AppSecRequestContext.class, "wafTimeouts");
147150
private static final AtomicIntegerFieldUpdater<AppSecRequestContext> RASP_TIMEOUTS_UPDATER =
148-
AtomicIntegerFieldUpdater.newUpdater(AppSecRequestContext.class, "raspTimeouts");
151+
AtomicIntegerFieldUpdater.newUpdater(AppSecRequestContext.class, "raspTimeouts");;
149152

150153
// to be called by the Event Dispatcher
151154
public void addAll(DataBundle newData) {
@@ -675,4 +678,20 @@ public boolean isWafContextClosed() {
675678
void setRequestEndCalled() {
676679
requestEndCalled = true;
677680
}
681+
682+
public void setProcessedRequestBody(Object processedRequestBody) {
683+
this.processedRequestBody = processedRequestBody;
684+
}
685+
686+
public Object getProcessedRequestBody() {
687+
return processedRequestBody;
688+
}
689+
690+
public boolean isRaspMatched() {
691+
return raspMatched;
692+
}
693+
694+
public void setRaspMatched(boolean raspMatched) {
695+
this.raspMatched = raspMatched;
696+
}
678697
}

dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ public class GatewayBridge {
7777
private static final String USER_COLLECTION_MODE_TAG = "_dd.appsec.user.collection_mode";
7878

7979
private static final Map<LoginEvent, Address<?>> EVENT_MAPPINGS = new EnumMap<>(LoginEvent.class);
80+
private static final String METASTRUCT_REQUEST_BODY = "http.request.body";
8081

8182
static {
8283
EVENT_MAPPINGS.put(LoginEvent.LOGIN_SUCCESS, KnownAddresses.LOGIN_SUCCESS);
@@ -572,9 +573,11 @@ private Flow<Void> onRequestBodyProcessed(RequestContext ctx_, Object obj) {
572573
if (subInfo == null || subInfo.isEmpty()) {
573574
return NoopFlow.INSTANCE;
574575
}
575-
DataBundle bundle =
576-
new SingletonDataBundle<>(
577-
KnownAddresses.REQUEST_BODY_OBJECT, ObjectIntrospection.convert(obj));
576+
Object converted = ObjectIntrospection.convert(obj);
577+
if (Config.get().isAppSecRaspCollectRequestBody()) {
578+
ctx.setProcessedRequestBody(converted);
579+
}
580+
DataBundle bundle = new SingletonDataBundle<>(KnownAddresses.REQUEST_BODY_OBJECT, converted);
578581
try {
579582
GatewayContext gwCtx = new GatewayContext(false);
580583
return producerService.publishDataEvent(subInfo, ctx, bundle, gwCtx);
@@ -720,6 +723,12 @@ private NoopFlow onRequestEnded(RequestContext ctx_, IGSpanInfo spanInfo) {
720723
StackUtils.addStacktraceEventsToMetaStruct(ctx_, METASTRUCT_EXPLOIT, stackTraces);
721724
}
722725

726+
// Report collected parsed request body if there is a RASP event
727+
if (ctx.isRaspMatched() && ctx.getProcessedRequestBody() != null) {
728+
ctx_.getOrCreateMetaStructTop(
729+
METASTRUCT_REQUEST_BODY, k -> ctx.getProcessedRequestBody());
730+
}
731+
723732
} else if (hasUserInfo(traceSeg)) {
724733
// Report all collected request headers on user tracking event
725734
writeRequestHeaders(traceSeg, REQUEST_HEADERS_ALLOW_LIST, ctx.getRequestHeaders(), false);

dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/ExtendedDataCollectionSmokeTest.groovy

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,64 @@
11
package datadog.smoketest.appsec
22

33
import datadog.trace.agent.test.utils.OkHttpUtils
4+
import okhttp3.FormBody
45
import okhttp3.Request
6+
import spock.lang.Shared
57

68
class ExtendedDataCollectionSmokeTest extends AbstractAppSecServerSmokeTest {
79

10+
@Shared
11+
String buildDir = new File(System.getProperty("datadog.smoketest.builddir")).absolutePath
12+
@Shared
13+
String customRulesPath = "${buildDir}/appsec_custom_rules.json"
14+
15+
def prepareCustomRules() {
16+
// Prepare ruleset with additional test rules
17+
mergeRules(
18+
customRulesPath,
19+
[
20+
[
21+
id : 'rasp-932-100', // to replace default rule
22+
name : 'Shell command injection exploit',
23+
enable : 'true',
24+
tags : [
25+
type: 'command_injection',
26+
category: 'vulnerability_trigger',
27+
cwe: '77',
28+
capec: '1000/152/248/88',
29+
confidence: '0',
30+
module: 'rasp'
31+
],
32+
conditions : [
33+
[
34+
parameters: [
35+
resource: [[address: 'server.sys.shell.cmd']],
36+
params : [[address: 'server.request.body']],
37+
],
38+
operator : "shi_detector",
39+
],
40+
],
41+
transformers: [],
42+
on_match : ['block']
43+
]
44+
])
45+
}
46+
847
@Override
948
ProcessBuilder createProcessBuilder() {
1049

50+
// We run this here to ensure it runs before starting the process. Child setupSpec runs after parent setupSpec,
51+
// so it is not a valid location.
52+
prepareCustomRules()
53+
1154
String springBootShadowJar = System.getProperty("datadog.smoketest.appsec.springboot.shadowJar.path")
1255

1356
List<String> command = new ArrayList<>()
1457
command.add(javaPath())
58+
//command.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005")
1559
command.addAll(defaultJavaProperties)
1660
command.addAll(defaultAppSecProperties)
61+
command.add('-Ddd.appsec.rasp.collect.request.body=true')
1762
command.add('-Ddd.appsec.collect.all.headers=true')
1863
command.add('-Ddd.appsec.header.collection.redaction.enabled=false')
1964
command.addAll((String[]) ["-jar", springBootShadowJar, "--server.port=${httpPort}"])
@@ -146,6 +191,83 @@ class ExtendedDataCollectionSmokeTest extends AbstractAppSecServerSmokeTest {
146191
rootSpan.meta.get('http.response.headers.content-language') == 'en-US'
147192
}
148193

194+
void 'test request body collection if RASP event'(){
195+
when:
196+
String url = "http://localhost:${httpPort}/shi/cmd"
197+
def formBuilder = new FormBody.Builder()
198+
formBuilder.add('cmd', '$(cat /etc/passwd 1>&2 ; echo .)')
199+
final body = formBuilder.build()
200+
def request = new Request.Builder()
201+
.url(url)
202+
.post(body)
203+
.build()
204+
def response = client.newCall(request).execute()
205+
def responseBodyStr = response.body().string()
206+
207+
then:
208+
response.code() == 403
209+
responseBodyStr.contains('You\'ve been blocked')
210+
211+
when:
212+
waitForTraceCount(1)
213+
214+
then:
215+
def rootSpans = this.rootSpans.toList()
216+
rootSpans.size() == 1
217+
def rootSpan = rootSpans[0]
218+
219+
def trigger = null
220+
for (t in rootSpan.triggers) {
221+
if (t['rule']['id'] == 'rasp-932-100') {
222+
trigger = t
223+
break
224+
}
225+
}
226+
assert trigger != null, 'test trigger not found'
227+
228+
rootSpan.span.metaStruct != null
229+
def requestBody = rootSpan.span.metaStruct.get('http.request.body')
230+
assert requestBody != null, 'request body is not set'
231+
232+
}
233+
234+
void 'test request body not collected if no RASP event'(){
235+
when:
236+
String url = "http://localhost:${httpPort}/greeting"
237+
def formBuilder = new FormBody.Builder()
238+
formBuilder.add('cmd', 'test')
239+
final body = formBuilder.build()
240+
def request = new Request.Builder()
241+
.url(url)
242+
.post(body)
243+
.build()
244+
def response = client.newCall(request).execute()
245+
def responseBodyStr = response.body().string()
246+
247+
then:
248+
response.code() == 200
249+
250+
when:
251+
waitForTraceCount(1)
252+
253+
then:
254+
def rootSpans = this.rootSpans.toList()
255+
rootSpans.size() == 1
256+
def rootSpan = rootSpans[0]
257+
258+
def trigger = null
259+
for (t in rootSpan.triggers) {
260+
if (t['rule']['id'] == 'rasp-932-100') {
261+
trigger = t
262+
break
263+
}
264+
}
265+
assert trigger == null, 'test trigger found'
266+
267+
rootSpan.span.metaStruct == null
268+
269+
}
270+
149271

150272

151273
}

dd-smoke-tests/appsec/springboot/src/test/groovy/datadog/smoketest/appsec/SpringBootSmokeTest.groovy

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,7 @@ class SpringBootSmokeTest extends AbstractAppSecServerSmokeTest {
469469
}
470470
}
471471
assert trigger != null, 'test trigger not found'
472+
rootSpan.span.metaStruct == null
472473

473474
where:
474475
variant | _
@@ -508,6 +509,7 @@ class SpringBootSmokeTest extends AbstractAppSecServerSmokeTest {
508509
}
509510
}
510511
assert trigger != null, 'test trigger not found'
512+
rootSpan.span.metaStruct == null
511513

512514
where:
513515
variant | _
@@ -600,6 +602,7 @@ class SpringBootSmokeTest extends AbstractAppSecServerSmokeTest {
600602
}
601603
}
602604
assert trigger != null, 'test trigger not found'
605+
rootSpan.span.metaStruct == null
603606

604607
where:
605608
endpoint | cmd | params
@@ -650,6 +653,7 @@ class SpringBootSmokeTest extends AbstractAppSecServerSmokeTest {
650653
}
651654
}
652655
assert trigger != null, 'test trigger not found'
656+
rootSpan.span.metaStruct == null
653657

654658
where:
655659
endpoint | cmd | params

dd-trace-api/src/main/java/datadog/trace/api/config/AppSecConfig.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public final class AppSecConfig {
4747
public static final String APPSEC_MAX_COLLECTED_HEADERS = "appsec.max.collected.headers";
4848
public static final String APPSEC_HEADER_COLLECTION_REDACTION_ENABLED =
4949
"appsec.header.collection.redaction.enabled";
50+
public static final String APPSEC_RASP_COLLECT_REQUEST_BODY = "appsec.rasp.collect.request.body";
5051

5152
private AppSecConfig() {}
5253
}

internal-api/src/main/java/datadog/trace/api/Config.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ public static String getHostName() {
295295
private final boolean appSecCollectAllHeaders;
296296
private final boolean appSecHeaderCollectionRedactionEnabled;
297297
private final int appSecMaxCollectedHeaders;
298+
private final boolean appSecRaspCollectRequestBody;
298299
private final boolean apiSecurityEnabled;
299300
private final float apiSecuritySampleDelay;
300301
private final boolean apiSecurityEndpointCollectionEnabled;
@@ -1395,6 +1396,8 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment())
13951396
appSecMaxCollectedHeaders =
13961397
configProvider.getInteger(
13971398
APPSEC_MAX_COLLECTED_HEADERS, DEFAULT_APPSEC_MAX_COLLECTED_HEADERS);
1399+
appSecRaspCollectRequestBody =
1400+
configProvider.getBoolean(APPSEC_RASP_COLLECT_REQUEST_BODY, false);
13981401
apiSecurityEnabled =
13991402
configProvider.getBoolean(
14001403
API_SECURITY_ENABLED, DEFAULT_API_SECURITY_ENABLED, API_SECURITY_ENABLED_EXPERIMENTAL);
@@ -4216,6 +4219,10 @@ public int getAppsecMaxCollectedHeaders() {
42164219
return appSecMaxCollectedHeaders;
42174220
}
42184221

4222+
public boolean isAppSecRaspCollectRequestBody() {
4223+
return appSecRaspCollectRequestBody;
4224+
}
4225+
42194226
public boolean isCloudPayloadTaggingEnabledFor(String serviceName) {
42204227
return cloudPayloadTaggingServices.contains(serviceName);
42214228
}

0 commit comments

Comments
 (0)