Skip to content

Commit 502cf6a

Browse files
committed
Collect parsed request body if RASP event
improve truncation wip wip - not working wip - fix
1 parent 9f23747 commit 502cf6a

File tree

8 files changed

+291
-44
lines changed

8 files changed

+291
-44
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
@@ -480,6 +480,7 @@ public void onDataAvailable(
480480
}
481481

482482
if (gwCtx.isRasp) {
483+
reqCtx.setRaspMatched(true);
483484
WafMetricCollector.get().raspRuleMatch(gwCtx.raspRuleType);
484485
}
485486

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

Lines changed: 19 additions & 0 deletions
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
@@ -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: 17 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);
@@ -472,7 +473,7 @@ private Flow<Void> onGrpcServerRequestMessage(RequestContext ctx_, Object obj) {
472473
if (subInfo == null || subInfo.isEmpty()) {
473474
return NoopFlow.INSTANCE;
474475
}
475-
Object convObj = ObjectIntrospection.convert(obj, ctx);
476+
Object convObj = ObjectIntrospection.convert(obj, ctx).getValue();
476477
DataBundle bundle =
477478
new SingletonDataBundle<>(KnownAddresses.GRPC_SERVER_REQUEST_MESSAGE, convObj);
478479
try {
@@ -572,9 +573,16 @@ private Flow<Void> onRequestBodyProcessed(RequestContext ctx_, Object obj) {
572573
if (subInfo == null || subInfo.isEmpty()) {
573574
return NoopFlow.INSTANCE;
574575
}
576+
ObjectIntrospection.ConversionResult<Object> converted =
577+
ObjectIntrospection.convert(obj, ctx);
578+
if (Config.get().isAppSecRaspCollectRequestBody()) {
579+
ctx.setProcessedRequestBody(converted.getValue());
580+
if (converted.isAnyTruncated()) {
581+
ctx_.getTraceSegment().setTagTop("_dd.appsec.rasp.request_body_size.exceeded", true);
582+
}
583+
}
575584
DataBundle bundle =
576-
new SingletonDataBundle<>(
577-
KnownAddresses.REQUEST_BODY_OBJECT, ObjectIntrospection.convert(obj, ctx));
585+
new SingletonDataBundle<>(KnownAddresses.REQUEST_BODY_OBJECT, converted.getValue());
578586
try {
579587
GatewayContext gwCtx = new GatewayContext(false);
580588
return producerService.publishDataEvent(subInfo, ctx, bundle, gwCtx);
@@ -723,6 +731,12 @@ private NoopFlow onRequestEnded(RequestContext ctx_, IGSpanInfo spanInfo) {
723731
StackUtils.addStacktraceEventsToMetaStruct(ctx_, METASTRUCT_EXPLOIT, stackTraces);
724732
}
725733

734+
// Report collected parsed request body if there is a RASP event
735+
if (ctx.isRaspMatched() && ctx.getProcessedRequestBody() != null) {
736+
ctx_.getOrCreateMetaStructTop(
737+
METASTRUCT_REQUEST_BODY, k -> ctx.getProcessedRequestBody());
738+
}
739+
726740
} else if (hasUserInfo(traceSeg)) {
727741
// Report all collected request headers on user tracking event
728742
writeRequestHeaders(traceSeg, REQUEST_HEADERS_ALLOW_LIST, ctx.getRequestHeaders(), false);

dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/event/data/ObjectIntrospectionSpecification.groovy

Lines changed: 79 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ class ObjectIntrospectionSpecification extends DDSpecification {
2828

2929
void 'null is preserved'() {
3030
expect:
31-
convert(null, ctx) == null
31+
convert(null, ctx).getValue() == null
3232
}
3333

3434
void 'type #type is preserved'() {
3535
when:
36-
def result = convert(input, ctx)
36+
def result = convert(input, ctx).getValue()
3737

3838
then:
3939
input.getClass() == type
@@ -56,7 +56,7 @@ class ObjectIntrospectionSpecification extends DDSpecification {
5656

5757
void 'type #type is converted to string'() {
5858
when:
59-
def result = convert(input, ctx)
59+
def result = convert(input, ctx).getValue()
6060

6161
then:
6262
type.isAssignableFrom(input.getClass())
@@ -83,9 +83,9 @@ class ObjectIntrospectionSpecification extends DDSpecification {
8383
}
8484

8585
expect:
86-
convert(iter, ctx) instanceof List
87-
convert(iter, ctx) == ['a', 'b']
88-
convert(['a', 'b'], ctx) == ['a', 'b']
86+
convert(iter, ctx).getValue() instanceof List
87+
convert(iter, ctx) .getValue()== ['a', 'b']
88+
convert(['a', 'b'], ctx).getValue() == ['a', 'b']
8989
}
9090

9191
void 'maps are converted to hash maps'() {
@@ -95,21 +95,21 @@ class ObjectIntrospectionSpecification extends DDSpecification {
9595
}
9696

9797
expect:
98-
convert(map, ctx) instanceof HashMap
99-
convert(map, ctx) == [a: 'b']
100-
convert([(6): 'b'], ctx) == ['6': 'b']
101-
convert([(null): 'b'], ctx) == ['null': 'b']
102-
convert([(true): 'b'], ctx) == ['true': 'b']
103-
convert([('a' as Character): 'b'], ctx) == ['a': 'b']
104-
convert([(createCharBuffer('a')): 'b'], ctx) == ['a': 'b']
98+
convert(map, ctx).getValue() instanceof HashMap
99+
convert(map, ctx).getValue() == [a: 'b']
100+
convert([(6): 'b'], ctx).getValue() == ['6': 'b']
101+
convert([(null): 'b'], ctx).getValue() == ['null': 'b']
102+
convert([(true): 'b'], ctx).getValue() == ['true': 'b']
103+
convert([('a' as Character): 'b'], ctx).getValue() == ['a': 'b']
104+
convert([(createCharBuffer('a')): 'b'], ctx).getValue() == ['a': 'b']
105105
}
106106

107107
void 'arrays are converted into lists'() {
108108
expect:
109-
convert([6, 'b'] as Object[], ctx) == [6, 'b']
110-
convert([null, null] as Object[], ctx) == [null, null]
111-
convert([1, 2] as int[], ctx) == [1 as int, 2 as int]
112-
convert([1, 2] as byte[], ctx) == [1 as byte, 2 as byte]
109+
convert([6, 'b'] as Object[], ctx).getValue() == [6, 'b']
110+
convert([null, null] as Object[], ctx).getValue() == [null, null]
111+
convert([1, 2] as int[], ctx).getValue() == [1 as int, 2 as int]
112+
convert([1, 2] as byte[], ctx).getValue() == [1 as byte, 2 as byte]
113113
}
114114

115115
@SuppressWarnings('UnusedPrivateField')
@@ -127,8 +127,8 @@ class ObjectIntrospectionSpecification extends DDSpecification {
127127

128128
void 'other objects are converted into hash maps'() {
129129
expect:
130-
convert(new ClassToBeConverted(), ctx) instanceof HashMap
131-
convert(new ClassToBeConvertedExt(), ctx) == [c: 'd', a: 'b', l: [1, 2]]
130+
convert(new ClassToBeConverted(), ctx).getValue() instanceof HashMap
131+
convert(new ClassToBeConvertedExt(), ctx) .getValue()== [c: 'd', a: 'b', l: [1, 2]]
132132
}
133133

134134
class ProtobufLikeClass {
@@ -139,15 +139,15 @@ class ObjectIntrospectionSpecification extends DDSpecification {
139139

140140
void 'some field names are ignored'() {
141141
expect:
142-
convert(new ProtobufLikeClass(), ctx) instanceof HashMap
143-
convert(new ProtobufLikeClass(), ctx) == [c: 'd']
142+
convert(new ProtobufLikeClass(), ctx).getValue() instanceof HashMap
143+
convert(new ProtobufLikeClass(), ctx).getValue() == [c: 'd']
144144
}
145145

146146
void 'invalid keys are converted to special strings'() {
147147
expect:
148-
convert(Collections.singletonMap(new ClassToBeConverted(), 'a'), ctx) == ['invalid_key:1': 'a']
149-
convert([new ClassToBeConverted(): 'a', new ClassToBeConverted(): 'b'], ctx) == ['invalid_key:1': 'a', 'invalid_key:2': 'b']
150-
convert(Collections.singletonMap([1, 2], 'a'), ctx) == ['invalid_key:1': 'a']
148+
convert(Collections.singletonMap(new ClassToBeConverted(), 'a'), ctx).getValue() == ['invalid_key:1': 'a']
149+
convert([new ClassToBeConverted(): 'a', new ClassToBeConverted(): 'b'], ctx).getValue() == ['invalid_key:1': 'a', 'invalid_key:2': 'b']
150+
convert(Collections.singletonMap([1, 2], 'a'), ctx).getValue() == ['invalid_key:1': 'a']
151151
}
152152

153153
void 'max number of elements is honored'() {
@@ -156,16 +156,28 @@ class ObjectIntrospectionSpecification extends DDSpecification {
156156
128.times { m[it] = 'b' }
157157

158158
when:
159-
def result1 = convert([['a'] * 255], ctx)[0]
160-
def result2 = convert([['a'] * 255 as String[]], ctx)[0]
159+
def result1 = convert([['a'] * 255], ctx)
160+
def result2 = convert([['a'] * 255 as String[]], ctx)
161161
def result3 = convert(m, ctx)
162162

163163
then:
164-
result1.size() == 254 // +2 for the lists
165-
result2.size() == 254 // +2 for the lists
166-
result3.size() == 127 // +1 for the map, 2 for each entry (key and value)
164+
result1.getValue()[0].size() == 254 // +2 for the lists
165+
result1.isAnyTruncated()
166+
result1.isCollectionTruncated()
167+
!result1.isDepthTruncated()
168+
!result1.isStringTruncated()
169+
result2.getValue()[0].size() == 254 // +2 for the lists
170+
result2.isAnyTruncated()
171+
result2.isCollectionTruncated()
172+
!result2.isDepthTruncated()
173+
!result2.isStringTruncated()
174+
result3.getValue().size() == 127// +1 for the map, 2 for each entry (key and value)
175+
!result3.isAnyTruncated()
176+
!result3.isCollectionTruncated()
177+
!result3.isDepthTruncated()
178+
!result3.isStringTruncated()
167179
2 * ctx.setWafTruncated()
168-
2 * wafMetricCollector.wafInputTruncated(false, true, false)
180+
//2 * wafMetricCollector.wafInputTruncated(false, true, false)
169181

170182
}
171183

@@ -179,18 +191,23 @@ class ObjectIntrospectionSpecification extends DDSpecification {
179191
when:
180192
// Invoke conversion with context
181193
def result = convert(objArray, ctx)
194+
def converted = result.getValue()
182195

183196
then:
184197
// Traverse converted arrays to count actual depth
185198
int depth = 0
186-
for (p = result; p != null; p = p[0]) {
199+
for (p = converted; p != null; p = p[0]) {
187200
depth++
188201
}
189202
depth == 21 // after max depth we have nulls
190203

191204
// Should record a truncation due to depth
192205
1 * ctx.setWafTruncated()
193-
1 * wafMetricCollector.wafInputTruncated(false, false, true)
206+
result.isDepthTruncated()
207+
!result.isCollectionTruncated()
208+
result.isAnyTruncated()
209+
!result.isStringTruncated()
210+
//1 * wafMetricCollector.wafInputTruncated(false, false, true)
194211
}
195212

196213
void 'max depth is honored — list version'() {
@@ -203,18 +220,23 @@ class ObjectIntrospectionSpecification extends DDSpecification {
203220
when:
204221
// Invoke conversion with context
205222
def result = convert(list, ctx)
223+
def converted = result.getValue()
206224

207225
then:
208226
// Traverse converted lists to count actual depth
209227
int depth = 0
210-
for (p = result; p != null; p = p[0]) {
228+
for (p = converted; p != null; p = p[0]) {
211229
depth++
212230
}
213231
depth == 21 // after max depth we have nulls
214232

215233
// Should record a truncation due to depth
216234
1 * ctx.setWafTruncated()
217-
1 * wafMetricCollector.wafInputTruncated(false, false, true)
235+
result.isDepthTruncated()
236+
!result.isCollectionTruncated()
237+
result.isAnyTruncated()
238+
!result.isStringTruncated()
239+
//1 * wafMetricCollector.wafInputTruncated(false, false, true)
218240
}
219241

220242
def 'max depth is honored — map version'() {
@@ -227,26 +249,32 @@ class ObjectIntrospectionSpecification extends DDSpecification {
227249
when:
228250
// Invoke conversion with context
229251
def result = convert(map, ctx)
252+
def converted = result.getValue()
230253

231254
then:
232255
// Traverse converted maps to count actual depth
233256
int depth = 0
234-
for (p = result; p != null; p = p['a']) {
257+
for (p = converted; p != null; p = p['a']) {
235258
depth++
236259
}
237260
depth == 21 // after max depth we have nulls
238261

239262
// Should record a truncation due to depth
240263
1 * ctx.setWafTruncated()
241-
1 * wafMetricCollector.wafInputTruncated(false, false, true)
264+
result.isDepthTruncated()
265+
!result.isCollectionTruncated()
266+
result.isAnyTruncated()
267+
!result.isStringTruncated()
268+
//1 * wafMetricCollector.wafInputTruncated(false, false, true)
242269
}
243270

244271
void 'truncate long #typeName to 4096 chars and set truncation flag'() {
245272
setup:
246273
def longInput = rawInput
247274

248275
when:
249-
def converted = convert(longInput, ctx)
276+
def result = convert(longInput, ctx)
277+
def converted = result.getValue()
250278

251279
then:
252280
// Should always produce a String of exactly 4096 chars
@@ -255,7 +283,11 @@ class ObjectIntrospectionSpecification extends DDSpecification {
255283

256284
// Should record a truncation due to string length
257285
1 * ctx.setWafTruncated()
258-
1 * wafMetricCollector.wafInputTruncated(true, false, false)
286+
//1 * wafMetricCollector.wafInputTruncated(true, false, false)
287+
result.isStringTruncated()
288+
!result.isDepthTruncated()
289+
!result.isCollectionTruncated()
290+
result.isAnyTruncated()
259291

260292
where:
261293
typeName | rawInput
@@ -271,7 +303,8 @@ class ObjectIntrospectionSpecification extends DDSpecification {
271303

272304
when:
273305
// convert returns Pair<convertedObject, wasTruncated>
274-
def converted = convert(inputMap, ctx)
306+
def result = convert(inputMap, ctx)
307+
def converted = result.getValue()
275308

276309
then:
277310
// Extract the single truncated key
@@ -281,7 +314,11 @@ class ObjectIntrospectionSpecification extends DDSpecification {
281314
truncatedKey.length() == 4096
282315

283316
1 * ctx.setWafTruncated()
284-
1 * wafMetricCollector.wafInputTruncated(true, false, false)
317+
//1 * wafMetricCollector.wafInputTruncated(true, false, false)
318+
result.isStringTruncated()
319+
!result.isDepthTruncated()
320+
!result.isCollectionTruncated()
321+
result.isAnyTruncated()
285322

286323

287324
where:
@@ -302,6 +339,7 @@ class ObjectIntrospectionSpecification extends DDSpecification {
302339
}
303340

304341
expect:
305-
convert([cs], ctx) == ['error:my exception']
342+
convert([cs], ctx).getValue() == ['error:my exception']
306343
}
307344
}
345+

0 commit comments

Comments
 (0)