Skip to content

Commit ca01312

Browse files
Add grpc.server.method to WAF addresses with FQN of the grpc method (#7079)
Add grpc.server.method to WAF addresses with FQN of the grpc method
1 parent 38271ed commit ca01312

16 files changed

Lines changed: 248 additions & 54 deletions

File tree

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ public interface KnownAddresses {
9393
Address<CaseInsensitiveMap<List<String>>> HEADERS_NO_COOKIES =
9494
new Address<>("server.request.headers.no_cookies");
9595

96+
Address<Object> GRPC_SERVER_METHOD = new Address<>("grpc.server.method");
97+
9698
Address<Object> GRPC_SERVER_REQUEST_MESSAGE = new Address<>("grpc.server.request.message");
9799

98100
// XXX: Not really used yet, but it's a known address and we should not treat it as unknown.
@@ -159,6 +161,8 @@ static Address<?> forName(String name) {
159161
return REQUEST_QUERY;
160162
case "server.request.headers.no_cookies":
161163
return HEADERS_NO_COOKIES;
164+
case "grpc.server.method":
165+
return GRPC_SERVER_METHOD;
162166
case "grpc.server.request.message":
163167
return GRPC_SERVER_REQUEST_MESSAGE;
164168
case "grpc.server.request.metadata":

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ public class GatewayBridge {
7979
private volatile DataSubscriberInfo requestBodySubInfo;
8080
private volatile DataSubscriberInfo pathParamsSubInfo;
8181
private volatile DataSubscriberInfo respDataSubInfo;
82+
private volatile DataSubscriberInfo grpcServerMethodSubInfo;
8283
private volatile DataSubscriberInfo grpcServerRequestMsgSubInfo;
8384
private volatile DataSubscriberInfo graphqlServerRequestMsgSubInfo;
8485
private volatile DataSubscriberInfo requestEndSubInfo;
@@ -361,6 +362,32 @@ public void init() {
361362
return maybePublishResponseData(ctx);
362363
});
363364

365+
subscriptionService.registerCallback(
366+
EVENTS.grpcServerMethod(),
367+
(ctx_, method) -> {
368+
AppSecRequestContext ctx = ctx_.getData(RequestContextSlot.APPSEC);
369+
if (ctx == null || method == null || method.isEmpty()) {
370+
return NoopFlow.INSTANCE;
371+
}
372+
while (true) {
373+
DataSubscriberInfo subInfo = grpcServerMethodSubInfo;
374+
if (subInfo == null) {
375+
subInfo = producerService.getDataSubscribers(KnownAddresses.GRPC_SERVER_METHOD);
376+
grpcServerMethodSubInfo = subInfo;
377+
}
378+
if (subInfo == null || subInfo.isEmpty()) {
379+
return NoopFlow.INSTANCE;
380+
}
381+
DataBundle bundle =
382+
new SingletonDataBundle<>(KnownAddresses.GRPC_SERVER_METHOD, method);
383+
try {
384+
return producerService.publishDataEvent(subInfo, ctx, bundle, true);
385+
} catch (ExpiredSubscriberInfoException e) {
386+
grpcServerMethodSubInfo = null;
387+
}
388+
}
389+
});
390+
364391
subscriptionService.registerCallback(
365392
EVENTS.grpcServerRequestMessage(),
366393
(ctx_, obj) -> {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class KnownAddressesSpecification extends Specification {
2828
'server.request.body.combined_file_size',
2929
'server.request.query',
3030
'server.request.headers.no_cookies',
31+
'grpc.server.method',
3132
'grpc.server.request.message',
3233
'grpc.server.request.metadata',
3334
'graphql.server.all_resolvers',
@@ -41,7 +42,7 @@ class KnownAddressesSpecification extends Specification {
4142

4243
void 'number of known addresses is expected number'() {
4344
expect:
44-
Address.instanceCount() == 29
45+
Address.instanceCount() == 30
4546
KnownAddresses.WAF_CONTEXT_PROCESSOR.serial == Address.instanceCount() - 1
4647
}
4748
}

dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class GatewayBridgeSpecification extends DDSpecification {
7777
BiFunction<RequestContext, Integer, Flow<Void>> responseStartedCB
7878
TriConsumer<RequestContext, String, String> respHeaderCB
7979
Function<RequestContext, Flow<Void>> respHeadersDoneCB
80+
BiFunction<RequestContext, String, Flow<Void>> grpcServerMethodCB
8081
BiFunction<RequestContext, Object, Flow<Void>> grpcServerRequestMessageCB
8182
BiFunction<RequestContext, Map<String, Object>, Flow<Void>> graphqlServerRequestMessageCB
8283
BiConsumer<RequestContext, String> databaseConnectionCB
@@ -413,6 +414,7 @@ class GatewayBridgeSpecification extends DDSpecification {
413414
1 * ig.registerCallback(EVENTS.responseStarted(), _) >> { responseStartedCB = it[1]; null }
414415
1 * ig.registerCallback(EVENTS.responseHeader(), _) >> { respHeaderCB = it[1]; null }
415416
1 * ig.registerCallback(EVENTS.responseHeaderDone(), _) >> { respHeadersDoneCB = it[1]; null }
417+
1 * ig.registerCallback(EVENTS.grpcServerMethod(), _) >> { grpcServerMethodCB = it[1]; null }
416418
1 * ig.registerCallback(EVENTS.grpcServerRequestMessage(), _) >> { grpcServerRequestMessageCB = it[1]; null }
417419
1 * ig.registerCallback(EVENTS.graphqlServerRequestMessage(), _) >> { graphqlServerRequestMessageCB = it[1]; null }
418420
1 * ig.registerCallback(EVENTS.databaseConnection(), _) >> { databaseConnectionCB = it[1]; null }
@@ -710,6 +712,22 @@ class GatewayBridgeSpecification extends DDSpecification {
710712
flow.action == Flow.Action.Noop.INSTANCE
711713
}
712714

715+
void 'grpc server method publishes'() {
716+
setup:
717+
eventDispatcher.getDataSubscribers(KnownAddresses.GRPC_SERVER_METHOD) >> nonEmptyDsInfo
718+
DataBundle bundle
719+
720+
when:
721+
Flow<?> flow = grpcServerMethodCB.apply(ctx, '/my.package.Greeter/SayHello')
722+
723+
then:
724+
1 * eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, true) >>
725+
{ args -> bundle = args[2]; NoopFlow.INSTANCE }
726+
bundle.get(KnownAddresses.GRPC_SERVER_METHOD) == '/my.package.Greeter/SayHello'
727+
flow.result == null
728+
flow.action == Flow.Action.Noop.INSTANCE
729+
}
730+
713731
void 'calls trace segment post processor'() {
714732
setup:
715733
AgentSpan span = Stub()

dd-java-agent/instrumentation/armeria-grpc/src/main/java/datadog/trace/instrumentation/armeria/grpc/server/TracingServerInterceptor.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import io.grpc.ForwardingServerCallListener;
2828
import io.grpc.Grpc;
2929
import io.grpc.Metadata;
30+
import io.grpc.MethodDescriptor;
3031
import io.grpc.ServerCall;
3132
import io.grpc.ServerCallHandler;
3233
import io.grpc.ServerInterceptor;
@@ -76,6 +77,7 @@ public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
7677
if (reqContext != null) {
7778
callIGCallbackClientAddress(cbp, reqContext, call);
7879
callIGCallbackHeaders(cbp, reqContext, headers);
80+
callIGCallbackGrpcServerMethod(cbp, reqContext, call.getMethodDescriptor());
7981
}
8082

8183
DECORATE.afterStart(span);
@@ -315,6 +317,16 @@ private static void callIGCallbackRequestEnded(@Nonnull final AgentSpan span) {
315317
}
316318
}
317319

320+
private static <ReqT, RespT> void callIGCallbackGrpcServerMethod(
321+
CallbackProvider cbp, RequestContext ctx, MethodDescriptor<ReqT, RespT> methodDescriptor) {
322+
String method = methodDescriptor.getFullMethodName();
323+
BiFunction<RequestContext, String, Flow<Void>> cb = cbp.getCallback(EVENTS.grpcServerMethod());
324+
if (method == null || cb == null) {
325+
return;
326+
}
327+
cb.apply(ctx, method);
328+
}
329+
318330
private static void callIGCallbackGrpcMessage(@Nonnull final AgentSpan span, Object obj) {
319331
if (obj == null) {
320332
return;

dd-java-agent/instrumentation/armeria-grpc/src/test/groovy/ArmeriaGrpcTest.groovy

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ abstract class ArmeriaGrpcTest extends VersionedNamingTestBase {
4545

4646
def collectedAppSecHeaders = [:]
4747
boolean appSecHeaderDone = false
48+
def collectedAppSecServerMethods = []
4849
def collectedAppSecReqMsgs = []
4950

5051
final Duration timeoutDuration() {
@@ -97,6 +98,10 @@ abstract class ArmeriaGrpcTest extends VersionedNamingTestBase {
9798
collectedAppSecReqMsgs << obj
9899
Flow.ResultFlow.empty()
99100
} as BiFunction<RequestContext, Object, Flow<Void>>)
101+
ig.registerCallback(EVENTS.grpcServerMethod(), { reqCtx, method ->
102+
collectedAppSecServerMethods << method
103+
Flow.ResultFlow.empty()
104+
} as BiFunction<RequestContext, String, Flow<Void>>)
100105
}
101106

102107
def cleanup() {
@@ -230,6 +235,8 @@ abstract class ArmeriaGrpcTest extends VersionedNamingTestBase {
230235
traceId.toLong() as String == collectedAppSecHeaders['x-datadog-trace-id']
231236
collectedAppSecReqMsgs.size() == 1
232237
collectedAppSecReqMsgs.first().name == name
238+
collectedAppSecServerMethods.size() == 1
239+
collectedAppSecServerMethods.first() == 'example.Greeter/SayHello'
233240

234241
and:
235242
if (isDataStreamsEnabled()) {

dd-java-agent/instrumentation/grpc-1.5/src/main/java/datadog/trace/instrumentation/grpc/server/TracingServerInterceptor.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import io.grpc.ForwardingServerCallListener;
2828
import io.grpc.Grpc;
2929
import io.grpc.Metadata;
30+
import io.grpc.MethodDescriptor;
3031
import io.grpc.ServerCall;
3132
import io.grpc.ServerCallHandler;
3233
import io.grpc.ServerInterceptor;
@@ -75,6 +76,7 @@ public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
7576
if (reqContext != null) {
7677
callIGCallbackClientAddress(cbp, reqContext, call);
7778
callIGCallbackHeaders(cbp, reqContext, headers);
79+
callIGCallbackGrpcServerMethod(cbp, reqContext, call.getMethodDescriptor());
7880
}
7981

8082
DECORATE.afterStart(span);
@@ -314,6 +316,16 @@ private static void callIGCallbackRequestEnded(@Nonnull final AgentSpan span) {
314316
}
315317
}
316318

319+
private static <ReqT, RespT> void callIGCallbackGrpcServerMethod(
320+
CallbackProvider cbp, RequestContext ctx, MethodDescriptor<ReqT, RespT> methodDescriptor) {
321+
String method = methodDescriptor.getFullMethodName();
322+
BiFunction<RequestContext, String, Flow<Void>> cb = cbp.getCallback(EVENTS.grpcServerMethod());
323+
if (method == null || cb == null) {
324+
return;
325+
}
326+
cb.apply(ctx, method);
327+
}
328+
317329
private static void callIGCallbackGrpcMessage(@Nonnull final AgentSpan span, Object obj) {
318330
if (obj == null) {
319331
return;

dd-java-agent/instrumentation/grpc-1.5/src/test/groovy/GrpcTest.groovy

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ abstract class GrpcTest extends VersionedNamingTestBase {
4747
def collectedAppSecHeaders = [:]
4848
boolean appSecHeaderDone = false
4949
def collectedAppSecReqMsgs = []
50+
def collectedAppSecServerMethods = []
5051

5152
@Override
5253
final String service() {
@@ -89,6 +90,10 @@ abstract class GrpcTest extends VersionedNamingTestBase {
8990
collectedAppSecReqMsgs << obj
9091
Flow.ResultFlow.empty()
9192
} as BiFunction<RequestContext, Object, Flow<Void>>)
93+
ig.registerCallback(EVENTS.grpcServerMethod(), { reqCtx, method ->
94+
collectedAppSecServerMethods << method
95+
Flow.ResultFlow.empty()
96+
} as BiFunction<RequestContext, String, Flow<Void>>)
9297
}
9398

9499
def cleanup() {
@@ -217,6 +222,8 @@ abstract class GrpcTest extends VersionedNamingTestBase {
217222
traceId.toLong() as String == collectedAppSecHeaders['x-datadog-trace-id']
218223
collectedAppSecReqMsgs.size() == 1
219224
collectedAppSecReqMsgs.first().name == name
225+
collectedAppSecServerMethods.size() == 1
226+
collectedAppSecServerMethods.first() == 'example.Greeter/SayHello'
220227

221228
and:
222229
if (isDataStreamsEnabled()) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package datadog.smoketest.appsec
2+
3+
abstract class AbstractSpringBootWithGRPCAppSecTest extends AbstractAppSecServerSmokeTest {
4+
5+
@Override
6+
ProcessBuilder createProcessBuilder() {
7+
String springBootShadowJar = System.getProperty("datadog.smoketest.appsec.springboot-grpc.shadowJar.path")
8+
assert springBootShadowJar != null
9+
10+
List<String> command = [
11+
javaPath(),
12+
*defaultJavaProperties,
13+
*defaultAppSecProperties,
14+
"-jar",
15+
springBootShadowJar,
16+
"--server.port=${httpPort}"
17+
].collect { it as String }
18+
19+
ProcessBuilder processBuilder = new ProcessBuilder(command)
20+
processBuilder.directory(new File(buildDirectory))
21+
}
22+
23+
static final String ROUTE = 'async_annotation_greeting'
24+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package datadog.smoketest.appsec
2+
3+
4+
import okhttp3.Request
5+
import spock.lang.Shared
6+
7+
class ServerMethodTest extends AbstractSpringBootWithGRPCAppSecTest {
8+
9+
@Shared
10+
String buildDir = new File(System.getProperty("datadog.smoketest.builddir")).absolutePath
11+
@Shared
12+
String customRulesPath = "${buildDir}/appsec_custom_rules.json"
13+
14+
@Override
15+
ProcessBuilder createProcessBuilder() {
16+
// We run this here to ensure it runs before starting the process. Child setupSpec runs after parent setupSpec,
17+
// so it is not a valid location.
18+
appendRules(customRulesPath, [
19+
[
20+
id : '__test_server_method_bock',
21+
name : 'test rule to block on server method',
22+
tags : [
23+
type : 'test',
24+
category : 'test',
25+
confidence: '1',
26+
],
27+
conditions : [
28+
[
29+
parameters: [
30+
inputs: [[address: 'grpc.server.method']],
31+
regex : 'Greeter',
32+
],
33+
operator : 'match_regex',
34+
]
35+
],
36+
transformers: [],
37+
on_match : ['block']
38+
]
39+
])
40+
return super.createProcessBuilder()
41+
}
42+
43+
void 'test grpc.server.method address'() {
44+
setup:
45+
String url = "http://localhost:${httpPort}/${ROUTE}"
46+
def request = new Request.Builder()
47+
.url("${url}?message=${'Hello!'.bytes.encodeBase64()}")
48+
.get().build()
49+
50+
when:
51+
def response = client.newCall(request).execute()
52+
53+
then:
54+
def responseBodyStr = response.body().string()
55+
responseBodyStr != null
56+
responseBodyStr.contains("bye")
57+
response.body().contentType().toString().contains("text/plain")
58+
response.code() == 200
59+
60+
and:
61+
waitForTraceCount(2) == 2
62+
rootSpans.size() == 2
63+
def grpcRootSpan = rootSpans.find { it.triggers }
64+
grpcRootSpan != null
65+
def match = grpcRootSpan.triggers[0]['rule_matches'][0]
66+
match != null
67+
match['parameters'][0]['address'] == 'grpc.server.method'
68+
match['parameters'][0]['value'] == 'smoketest.Greeter/Hello'
69+
}
70+
}

0 commit comments

Comments
 (0)