Skip to content

Commit 6c26663

Browse files
Add downstream HTTP request/response analysis for OkHttp2 client (#9572)
Implementation of http downstream request analysis for OkHttp2
1 parent 5912cf3 commit 6c26663

File tree

38 files changed

+2055
-87
lines changed

38 files changed

+2055
-87
lines changed

dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpClientDecorator.java

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import datadog.trace.api.DDTags;
1111
import datadog.trace.api.InstrumenterConfig;
1212
import datadog.trace.api.ProductActivation;
13+
import datadog.trace.api.appsec.HttpClientRequest;
1314
import datadog.trace.api.gateway.BlockResponseFunction;
1415
import datadog.trace.api.gateway.Flow;
1516
import datadog.trace.api.gateway.RequestContext;
@@ -99,7 +100,7 @@ public AgentSpan onRequest(final AgentSpan span, final REQUEST request) {
99100
HTTP_RESOURCE_DECORATOR.withClientPath(span, method, url.getPath());
100101
}
101102
// SSRF exploit prevention check
102-
onNetworkConnection(url.toString());
103+
onHttpClientRequest(span, url.toString());
103104
} else if (shouldSetResourceName()) {
104105
span.setResourceName(DEFAULT_RESOURCE_NAME);
105106
}
@@ -178,24 +179,19 @@ public long getResponseContentLength(final RESPONSE response) {
178179
return 0;
179180
}
180181

181-
private void onNetworkConnection(final String networkConnection) {
182+
protected void onHttpClientRequest(final AgentSpan span, final String url) {
182183
if (!APPSEC_RASP_ENABLED) {
183184
return;
184185
}
185-
if (networkConnection == null) {
186+
if (url == null) {
186187
return;
187188
}
188-
final BiFunction<RequestContext, String, Flow<Void>> networkConnectionCallback =
189+
final BiFunction<RequestContext, HttpClientRequest, Flow<Void>> requestCb =
189190
AgentTracer.get()
190191
.getCallbackProvider(RequestContextSlot.APPSEC)
191-
.getCallback(EVENTS.networkConnection());
192+
.getCallback(EVENTS.httpClientRequest());
192193

193-
if (networkConnectionCallback == null) {
194-
return;
195-
}
196-
197-
final AgentSpan span = AgentTracer.get().activeSpan();
198-
if (span == null) {
194+
if (requestCb == null) {
199195
return;
200196
}
201197

@@ -204,7 +200,8 @@ private void onNetworkConnection(final String networkConnection) {
204200
return;
205201
}
206202

207-
Flow<Void> flow = networkConnectionCallback.apply(ctx, networkConnection);
203+
final long requestId = span.getSpanId();
204+
Flow<Void> flow = requestCb.apply(ctx, new HttpClientRequest(requestId, url));
208205
Flow.Action action = flow.getAction();
209206
if (action instanceof Flow.Action.RequestBlockingAction) {
210207
BlockResponseFunction brf = ctx.getBlockResponseFunction();

dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/HttpClientDecoratorTest.groovy

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package datadog.trace.bootstrap.instrumentation.decorator
22

33
import datadog.trace.api.DDTags
4+
import datadog.trace.api.appsec.HttpClientRequest
45
import datadog.trace.api.config.AppSecConfig
56
import datadog.trace.api.gateway.CallbackProvider
67
import static datadog.trace.api.gateway.Events.EVENTS
@@ -249,8 +250,8 @@ class HttpClientDecoratorTest extends ClientDecoratorTest {
249250
decorator.onRequest(span2, req)
250251

251252
then:
252-
1 * callbackProvider.getCallback(EVENTS.networkConnection()) >> listener
253-
1 * listener.apply(reqCtx, _ as String)
253+
1 * callbackProvider.getCallback(EVENTS.httpClientRequest()) >> listener
254+
1 * listener.apply(reqCtx, _ as HttpClientRequest)
254255
}
255256

256257
@Override
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.datadog.appsec.api.security;
2+
3+
import com.datadog.appsec.gateway.AppSecRequestContext;
4+
5+
public interface ApiSecurityDownstreamSampler {
6+
7+
boolean sampleHttpClientRequest(AppSecRequestContext ctx, long requestId);
8+
9+
boolean isSampled(AppSecRequestContext ctx, long requestId);
10+
11+
class NoOp implements ApiSecurityDownstreamSampler {
12+
13+
public static final NoOp INSTANCE = new NoOp();
14+
15+
@Override
16+
public boolean sampleHttpClientRequest(AppSecRequestContext ctx, long requestId) {
17+
return false;
18+
}
19+
20+
@Override
21+
public boolean isSampled(AppSecRequestContext ctx, long requestId) {
22+
return false;
23+
}
24+
}
25+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.datadog.appsec.api.security;
2+
3+
import com.datadog.appsec.gateway.AppSecRequestContext;
4+
import datadog.trace.api.Config;
5+
import java.util.concurrent.atomic.AtomicLong;
6+
7+
public class ApiSecurityDownstreamSamplerImpl implements ApiSecurityDownstreamSampler {
8+
9+
private static final long KNUTH_FACTOR = 1111111111111111111L;
10+
private final AtomicLong globalRequestCount;
11+
private final double threshold;
12+
13+
public ApiSecurityDownstreamSamplerImpl() {
14+
this(Config.get().getApiSecurityDownstreamRequestAnalysisSampleRate());
15+
}
16+
17+
public ApiSecurityDownstreamSamplerImpl(final double rate) {
18+
threshold = samplingCutoff(rate < 0.0 ? 0 : (rate > 1.0 ? 1 : rate));
19+
globalRequestCount = new AtomicLong(0);
20+
}
21+
22+
private static double samplingCutoff(final double rate) {
23+
final double max = Math.pow(2, 64) - 1;
24+
if (rate < 0.5) {
25+
return (long) (rate * max) + Long.MIN_VALUE;
26+
}
27+
if (rate < 1.0) {
28+
return (long) ((rate * max) + Long.MIN_VALUE);
29+
}
30+
return Long.MAX_VALUE;
31+
}
32+
33+
/**
34+
* First sample the request to ensure we randomize the request and then check if the current
35+
* server request has budget to analyze the downstream request.
36+
*/
37+
@Override
38+
public boolean sampleHttpClientRequest(final AppSecRequestContext ctx, final long requestId) {
39+
final long counter = updateRequestCount();
40+
if (counter * KNUTH_FACTOR + Long.MIN_VALUE > threshold) {
41+
return false;
42+
}
43+
return ctx.sampleHttpClientRequest(requestId);
44+
}
45+
46+
@Override
47+
public boolean isSampled(final AppSecRequestContext ctx, final long requestId) {
48+
return ctx.isHttpClientRequestSampled(requestId);
49+
}
50+
51+
private long updateRequestCount() {
52+
return globalRequestCount.updateAndGet(cur -> (cur == Long.MAX_VALUE) ? 0L : cur + 1L);
53+
}
54+
}

dd-java-agent/appsec/src/main/java/com/datadog/appsec/event/data/KnownAddresses.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,26 @@ public interface KnownAddresses {
118118
/** The URL of a network resource being requested (outgoing request) */
119119
Address<String> IO_NET_URL = new Address<>("server.io.net.url");
120120

121+
/** The headers of a network resource being requested (outgoing request) */
122+
Address<Map<String, List<String>>> IO_NET_REQUEST_HEADERS =
123+
new Address<>("server.io.net.request.headers");
124+
125+
/** The method of a network resource being requested (outgoing request) */
126+
Address<String> IO_NET_REQUEST_METHOD = new Address<>("server.io.net.request.method");
127+
128+
/** The body of a network resource being requested (outgoing request) */
129+
Address<Object> IO_NET_REQUEST_BODY = new Address<>("server.io.net.request.body");
130+
131+
/** The status of a network resource being requested (outgoing request) */
132+
Address<String> IO_NET_RESPONSE_STATUS = new Address<>("server.io.net.response.status");
133+
134+
/** The response headers of a network resource being requested (outgoing request) */
135+
Address<Map<String, List<String>>> IO_NET_RESPONSE_HEADERS =
136+
new Address<>("server.io.net.response.headers");
137+
138+
/** The response body of a network resource being requested (outgoing request) */
139+
Address<Object> IO_NET_RESPONSE_BODY = new Address<>("server.io.net.response.body");
140+
121141
/** The representation of opened file on the filesystem */
122142
Address<String> IO_FS_FILE = new Address<>("server.io.fs.file");
123143

@@ -206,6 +226,18 @@ static Address<?> forName(String name) {
206226
return SESSION_ID;
207227
case "server.io.net.url":
208228
return IO_NET_URL;
229+
case "server.io.net.request.headers":
230+
return IO_NET_REQUEST_HEADERS;
231+
case "server.io.net.request.method":
232+
return IO_NET_REQUEST_METHOD;
233+
case "server.io.net.request.body":
234+
return IO_NET_REQUEST_BODY;
235+
case "server.io.net.response.status":
236+
return IO_NET_RESPONSE_STATUS;
237+
case "server.io.net.response.headers":
238+
return IO_NET_RESPONSE_HEADERS;
239+
case "server.io.net.response.body":
240+
return IO_NET_RESPONSE_BODY;
209241
case "server.io.fs.file":
210242
return IO_FS_FILE;
211243
case "server.db.system":

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,9 @@ public class AppSecRequestContext implements DataBundle, Closeable {
149149
private volatile Long apiSecurityEndpointHash;
150150
private volatile byte keepType = PrioritySampling.SAMPLER_KEEP;
151151

152+
private final AtomicInteger httpClientRequestCount = new AtomicInteger(0);
153+
private final Set<Long> sampledHttpClientRequests = new HashSet<>();
154+
152155
private static final AtomicIntegerFieldUpdater<AppSecRequestContext> WAF_TIMEOUTS_UPDATER =
153156
AtomicIntegerFieldUpdater.newUpdater(AppSecRequestContext.class, "wafTimeouts");
154157
private static final AtomicIntegerFieldUpdater<AppSecRequestContext> RASP_TIMEOUTS_UPDATER =
@@ -235,6 +238,26 @@ public void increaseRaspTimeouts() {
235238
RASP_TIMEOUTS_UPDATER.incrementAndGet(this);
236239
}
237240

241+
public boolean sampleHttpClientRequest(final long id) {
242+
httpClientRequestCount.incrementAndGet();
243+
synchronized (sampledHttpClientRequests) {
244+
if (sampledHttpClientRequests.size()
245+
< Config.get().getApiSecurityMaxDownstreamRequestBodyAnalysis()) {
246+
sampledHttpClientRequests.add(id);
247+
return true;
248+
}
249+
}
250+
return false;
251+
}
252+
253+
public boolean isHttpClientRequestSampled(final long id) {
254+
return sampledHttpClientRequests.contains(id);
255+
}
256+
257+
public int getHttpClientRequestCount() {
258+
return httpClientRequestCount.get();
259+
}
260+
238261
public int getWafTimeouts() {
239262
return wafTimeouts;
240263
}

0 commit comments

Comments
 (0)