Skip to content

Commit e92e233

Browse files
Add support for endpoint discovery in spring mvc
1 parent 8a74e85 commit e92e233

File tree

20 files changed

+650
-0
lines changed

20 files changed

+650
-0
lines changed

dd-java-agent/appsec/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ dependencies {
2323
testImplementation project(':utils:test-utils')
2424
testImplementation group: 'org.hamcrest', name: 'hamcrest', version: '2.2'
2525
testImplementation group: 'com.flipkart.zjsonpatch', name: 'zjsonpatch', version: '0.4.11'
26+
testImplementation(group: 'org.skyscreamer', name: 'jsonassert', version: '1.5.1')
2627

2728
testFixturesApi project(':dd-java-agent:testing')
2829
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.datadog.appsec.api.security.json;
2+
3+
import com.squareup.moshi.JsonWriter;
4+
import com.squareup.moshi.ToJson;
5+
import datadog.trace.api.appsec.api.security.model.Endpoint;
6+
import java.io.IOException;
7+
import javax.annotation.Nonnull;
8+
import javax.annotation.Nullable;
9+
10+
public class EndpointAdapter {
11+
12+
@ToJson
13+
public void toJson(@Nonnull final JsonWriter jsonWriter, @Nullable final Endpoint endpoint)
14+
throws IOException {
15+
if (endpoint == null) {
16+
jsonWriter.nullValue();
17+
} else {
18+
jsonWriter.beginObject();
19+
jsonWriter.name("type");
20+
jsonWriter.value(endpoint.getType().name());
21+
jsonWriter.name("method");
22+
jsonWriter.value(endpoint.getMethod().getName());
23+
jsonWriter.name("path");
24+
jsonWriter.value(endpoint.getPath());
25+
jsonWriter.name("operation-name");
26+
jsonWriter.value(endpoint.getOperation().getName());
27+
jsonWriter.name("request-body-type");
28+
jsonWriter.jsonValue(endpoint.getRequestBodyType());
29+
jsonWriter.name("response-body-type");
30+
jsonWriter.jsonValue(endpoint.getResponseBodyType());
31+
jsonWriter.name("response-code");
32+
jsonWriter.jsonValue(endpoint.getResponseCode());
33+
jsonWriter.name("authentication");
34+
jsonWriter.jsonValue(endpoint.getAuthentication());
35+
jsonWriter.name("metadata");
36+
jsonWriter.jsonValue(endpoint.getMetadata());
37+
jsonWriter.endObject();
38+
}
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.datadog.appsec.api.security.json;
2+
3+
import com.squareup.moshi.JsonAdapter;
4+
import com.squareup.moshi.Moshi;
5+
import datadog.trace.api.appsec.api.security.model.Endpoint;
6+
import java.util.List;
7+
8+
public class EndpointsEncoding {
9+
10+
private static final JsonAdapter<Endpoints> JSON_ADAPTER =
11+
new Moshi.Builder().add(new EndpointAdapter()).build().adapter(Endpoints.class);
12+
13+
public static String toJson(final List<Endpoint> endpoints) {
14+
final Endpoints target = new Endpoints();
15+
target.setEndpoints(endpoints);
16+
return JSON_ADAPTER.toJson(target);
17+
}
18+
19+
public static class Endpoints {
20+
21+
private List<Endpoint> endpoints;
22+
23+
public List<Endpoint> getEndpoints() {
24+
return endpoints;
25+
}
26+
27+
public void setEndpoints(final List<Endpoint> endpoints) {
28+
this.endpoints = endpoints;
29+
}
30+
}
31+
}

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

+16
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.datadog.appsec.report.AppSecEventWrapper;
2929
import datadog.trace.api.Config;
3030
import datadog.trace.api.UserIdCollectionMode;
31+
import datadog.trace.api.appsec.api.security.model.Endpoint;
3132
import datadog.trace.api.gateway.Events;
3233
import datadog.trace.api.gateway.Flow;
3334
import datadog.trace.api.gateway.IGSpanInfo;
@@ -55,13 +56,17 @@
5556
import java.util.Collections;
5657
import java.util.HashMap;
5758
import java.util.HashSet;
59+
import java.util.Iterator;
5860
import java.util.List;
5961
import java.util.Map;
6062
import java.util.Set;
63+
import java.util.Spliterator;
64+
import java.util.Spliterators;
6165
import java.util.concurrent.ConcurrentHashMap;
6266
import java.util.concurrent.atomic.AtomicBoolean;
6367
import java.util.regex.Pattern;
6468
import java.util.stream.Collectors;
69+
import java.util.stream.StreamSupport;
6570
import org.slf4j.Logger;
6671
import org.slf4j.LoggerFactory;
6772

@@ -171,6 +176,10 @@ public void init() {
171176
subscriptionService.registerCallback(
172177
EVENTS.requestBodyProcessed(), this::onRequestBodyProcessed);
173178
}
179+
180+
if (Config.get().isApiSecurityEndpointCollectionEnabled()) {
181+
subscriptionService.registerCallback(EVENTS.endpoints(), this::onEndpoints);
182+
}
174183
}
175184

176185
/**
@@ -197,6 +206,13 @@ public void reset() {
197206
shellCmdSubInfo = null;
198207
}
199208

209+
private void onEndpoints(final Iterator<Endpoint> endpoints) {
210+
// TODO: do something with the endpoints
211+
StreamSupport.stream(Spliterators.spliteratorUnknownSize(endpoints, Spliterator.ORDERED), false)
212+
.limit(Config.get().getApiSecurityEndpointCollectionMessageLimit())
213+
.forEach(System.out::println);
214+
}
215+
200216
private Flow<Void> onUser(
201217
final RequestContext ctx_, final UserIdCollectionMode mode, final String originalUser) {
202218
if (mode == DISABLED) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.datadog.appsec.api.security.model
2+
3+
import com.datadog.appsec.api.security.json.EndpointsEncoding
4+
import datadog.trace.api.appsec.api.security.model.Endpoint
5+
import datadog.trace.test.util.DDSpecification
6+
import org.skyscreamer.jsonassert.JSONAssert
7+
import org.skyscreamer.jsonassert.JSONCompareMode
8+
9+
import static datadog.trace.api.appsec.api.security.model.Endpoint.Method.POST
10+
import static datadog.trace.api.appsec.api.security.model.Endpoint.Operation.HTTP_REQUEST
11+
import static datadog.trace.api.appsec.api.security.model.Endpoint.Type.REST
12+
13+
class EndpointsEncodingTest extends DDSpecification {
14+
15+
void 'test json encoding of endpoints'() {
16+
when:
17+
final json = EndpointsEncoding.toJson(test.v1)
18+
19+
then:
20+
JSONAssert.assertEquals("Endpoints payload should match", test.v2, json, JSONCompareMode.NON_EXTENSIBLE)
21+
22+
where:
23+
test << buildEndpoints()
24+
}
25+
26+
static List<Tuple2<List<Endpoint>, String>> buildEndpoints() {
27+
return [
28+
Tuple.tuple([
29+
new Endpoint(type: REST,
30+
method: POST,
31+
path: '/analytics/requests',
32+
operation: HTTP_REQUEST,
33+
requestBodyType: ['application/json'],
34+
responseBodyType: ['application/json'],
35+
responseCode: [200, 201],
36+
authentication: ['JWT'],
37+
metadata: ['dotnet-ignore-anti-forgery': true, 'deprecated': true])
38+
],
39+
"""
40+
{
41+
"endpoints": [
42+
{
43+
"type": "REST",
44+
"method": "POST",
45+
"path": "/analytics/requests",
46+
"operation-name": "http.request",
47+
"request-body-type": ["application/json"],
48+
"response-body-type": ["application/json"],
49+
"response-code": [200, 201],
50+
"authentication": ["JWT"],
51+
"metadata": {
52+
"dotnet-ignore-anti-forgery": true,
53+
"deprecated": true
54+
}
55+
}
56+
]
57+
}
58+
""")
59+
]
60+
}
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package datadog.trace.instrumentation.springweb;
2+
3+
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
4+
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
5+
import static net.bytebuddy.matcher.ElementMatchers.isProtected;
6+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
7+
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
8+
9+
import com.google.auto.service.AutoService;
10+
import datadog.trace.agent.tooling.Instrumenter;
11+
import datadog.trace.agent.tooling.InstrumenterModule;
12+
import datadog.trace.api.appsec.api.security.model.Endpoint;
13+
import datadog.trace.api.gateway.CallbackProvider;
14+
import datadog.trace.api.gateway.Events;
15+
import datadog.trace.api.gateway.RequestContextSlot;
16+
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
17+
import java.util.Iterator;
18+
import java.util.Map;
19+
import java.util.function.Consumer;
20+
import net.bytebuddy.asm.Advice;
21+
import org.springframework.context.ApplicationContext;
22+
import org.springframework.web.method.HandlerMethod;
23+
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
24+
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
25+
26+
@AutoService(InstrumenterModule.class)
27+
public class AppSecDispatcherServletInstrumentation extends InstrumenterModule.AppSec
28+
implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice {
29+
30+
public AppSecDispatcherServletInstrumentation() {
31+
super("spring-web");
32+
}
33+
34+
@Override
35+
public String instrumentedType() {
36+
return "org.springframework.web.servlet.DispatcherServlet";
37+
}
38+
39+
@Override
40+
public String[] helperClassNames() {
41+
return new String[] {packageName + ".RequestMappingInfoInterator"};
42+
}
43+
44+
@Override
45+
public void methodAdvice(MethodTransformer transformer) {
46+
transformer.applyAdvice(
47+
isMethod()
48+
.and(isProtected())
49+
.and(named("onRefresh"))
50+
.and(takesArgument(0, named("org.springframework.context.ApplicationContext")))
51+
.and(takesArguments(1)),
52+
AppSecDispatcherServletInstrumentation.class.getName() + "$AppSecHandlerMappingAdvice");
53+
}
54+
55+
public static class AppSecHandlerMappingAdvice {
56+
57+
@Advice.OnMethodExit(suppress = Throwable.class)
58+
public static void afterRefresh(@Advice.Argument(0) final ApplicationContext springCtx) {
59+
60+
final CallbackProvider cbp = AgentTracer.get().getCallbackProvider(RequestContextSlot.APPSEC);
61+
if (cbp == null) {
62+
return;
63+
}
64+
final Consumer<Iterator<Endpoint>> callback = cbp.getCallback(Events.get().endpoints());
65+
if (callback == null) {
66+
return;
67+
}
68+
final RequestMappingHandlerMapping handler =
69+
springCtx.getBean(RequestMappingHandlerMapping.class);
70+
if (handler == null) {
71+
return;
72+
}
73+
final Map<RequestMappingInfo, HandlerMethod> mappings = handler.getHandlerMethods();
74+
if (mappings == null || mappings.isEmpty()) {
75+
return;
76+
}
77+
callback.accept(new RequestMappingInfoInterator(mappings));
78+
}
79+
}
80+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package datadog.trace.instrumentation.springweb;
2+
3+
import static datadog.trace.api.appsec.api.security.model.Endpoint.Operation.HTTP_REQUEST;
4+
import static datadog.trace.api.appsec.api.security.model.Endpoint.Type.REST;
5+
6+
import datadog.trace.api.appsec.api.security.model.Endpoint;
7+
import datadog.trace.api.appsec.api.security.model.Endpoint.Method;
8+
import java.util.ArrayList;
9+
import java.util.HashMap;
10+
import java.util.Iterator;
11+
import java.util.LinkedList;
12+
import java.util.List;
13+
import java.util.Map;
14+
import java.util.NoSuchElementException;
15+
import java.util.Queue;
16+
import java.util.Set;
17+
import org.springframework.web.bind.annotation.RequestMethod;
18+
import org.springframework.web.method.HandlerMethod;
19+
import org.springframework.web.servlet.mvc.condition.MediaTypeExpression;
20+
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
21+
22+
public class RequestMappingInfoInterator implements Iterator<Endpoint> {
23+
24+
private final Iterator<Map.Entry<RequestMappingInfo, HandlerMethod>> delegate;
25+
private final Queue<Endpoint> queue = new LinkedList<>();
26+
27+
public RequestMappingInfoInterator(final Map<RequestMappingInfo, HandlerMethod> mappings) {
28+
delegate = mappings.entrySet().iterator();
29+
fetchNext();
30+
}
31+
32+
@Override
33+
public boolean hasNext() {
34+
return !queue.isEmpty();
35+
}
36+
37+
@Override
38+
public Endpoint next() {
39+
Endpoint result = queue.poll();
40+
if (result == null) {
41+
throw new NoSuchElementException();
42+
}
43+
if (queue.isEmpty()) {
44+
fetchNext();
45+
}
46+
return result;
47+
}
48+
49+
private void fetchNext() {
50+
if (!delegate.hasNext()) {
51+
return;
52+
}
53+
final Map.Entry<RequestMappingInfo, HandlerMethod> nextEntry = delegate.next();
54+
final RequestMappingInfo nextInfo = nextEntry.getKey();
55+
final HandlerMethod nextHandler = nextEntry.getValue();
56+
for (final String path : nextInfo.getPatternsCondition().getPatterns()) {
57+
final List<Method> methods = new LinkedList<>();
58+
if (nextInfo.getMethodsCondition().getMethods().isEmpty()) {
59+
methods.add(Method.ALL);
60+
} else {
61+
for (final RequestMethod method : nextInfo.getMethodsCondition().getMethods()) {
62+
methods.add(Method.parseMethod(method.name()));
63+
}
64+
}
65+
for (final Method method : methods) {
66+
final Endpoint endpoint = new Endpoint();
67+
endpoint.setType(REST);
68+
endpoint.setOperation(HTTP_REQUEST);
69+
endpoint.setPath(path);
70+
endpoint.setMethod(method);
71+
endpoint.setRequestBodyType(
72+
parseMediaTypes(nextInfo.getConsumesCondition().getExpressions()));
73+
endpoint.setResponseBodyType(
74+
parseMediaTypes(nextInfo.getProducesCondition().getExpressions()));
75+
final Map<String, Object> metadata = new HashMap<>();
76+
metadata.put("handler", nextHandler.toString());
77+
endpoint.setMetadata(metadata);
78+
queue.add(endpoint);
79+
}
80+
}
81+
}
82+
83+
private List<String> parseMediaTypes(final Set<MediaTypeExpression> expressions) {
84+
if (expressions.isEmpty()) {
85+
return null;
86+
}
87+
final List<String> result = new ArrayList<>(expressions.size());
88+
for (final MediaTypeExpression expression : expressions) {
89+
result.add(expression.toString());
90+
}
91+
return result;
92+
}
93+
}

0 commit comments

Comments
 (0)