diff --git a/dd-smoke-tests/resteasy/build.gradle b/dd-smoke-tests/resteasy/build.gradle index 089d9a44052..5b7fd20deda 100644 --- a/dd-smoke-tests/resteasy/build.gradle +++ b/dd-smoke-tests/resteasy/build.gradle @@ -16,6 +16,7 @@ dependencies { implementation group: 'org.jboss.resteasy', name: 'resteasy-undertow', version:'3.1.0.Final' implementation group: 'org.jboss.resteasy', name: 'resteasy-cdi', version:'3.1.0.Final' implementation group: 'org.jboss.weld.servlet', name: 'weld-servlet', version: '2.4.8.Final' + implementation group: 'org.jboss.resteasy', name: 'resteasy-jackson2-provider', version: '3.1.0.Final' implementation group: 'javax.el', name: 'javax.el-api', version:'3.0.0' @@ -24,6 +25,7 @@ dependencies { testImplementation project(':dd-smoke-tests') testImplementation(testFixtures(project(":dd-smoke-tests:iast-util"))) + testImplementation project(':dd-smoke-tests:appsec') } tasks.withType(Test).configureEach { diff --git a/dd-smoke-tests/resteasy/src/main/java/smoketest/resteasy/App.java b/dd-smoke-tests/resteasy/src/main/java/smoketest/resteasy/App.java index e5bfb87b2b3..2ef9aae4206 100644 --- a/dd-smoke-tests/resteasy/src/main/java/smoketest/resteasy/App.java +++ b/dd-smoke-tests/resteasy/src/main/java/smoketest/resteasy/App.java @@ -1,8 +1,10 @@ package smoketest.resteasy; +import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider; import java.util.HashSet; import java.util.Set; import javax.ws.rs.core.Application; +import org.jboss.resteasy.plugins.providers.StringTextStar; public class App extends Application { @@ -10,6 +12,8 @@ public class App extends Application { public App() { singletons.add(new Resource()); + singletons.add(new StringTextStar()); // Writer for String + singletons.add(new JacksonJsonProvider()); // Writer for json } @Override diff --git a/dd-smoke-tests/resteasy/src/main/java/smoketest/resteasy/RequestBody.java b/dd-smoke-tests/resteasy/src/main/java/smoketest/resteasy/RequestBody.java new file mode 100644 index 00000000000..a77d27c7100 --- /dev/null +++ b/dd-smoke-tests/resteasy/src/main/java/smoketest/resteasy/RequestBody.java @@ -0,0 +1,45 @@ +package smoketest.resteasy; + +import java.util.List; + +public class RequestBody { + private List main; + private Object nullable; + + public List getMain() { + return main; + } + + public void setMain(List main) { + this.main = main; + } + + public Object getNullable() { + return nullable; + } + + public void setNullable(Object nullable) { + this.nullable = nullable; + } + + public static class KeyValue { + private String key; + private Double value; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public Double getValue() { + return value; + } + + public void setValue(Double value) { + this.value = value; + } + } +} diff --git a/dd-smoke-tests/resteasy/src/main/java/smoketest/resteasy/Resource.java b/dd-smoke-tests/resteasy/src/main/java/smoketest/resteasy/Resource.java index 630a275e53e..bc8ff6fcfc0 100644 --- a/dd-smoke-tests/resteasy/src/main/java/smoketest/resteasy/Resource.java +++ b/dd-smoke-tests/resteasy/src/main/java/smoketest/resteasy/Resource.java @@ -6,9 +6,11 @@ import java.util.List; import java.util.Set; import java.util.SortedSet; +import javax.ws.rs.Consumes; import javax.ws.rs.CookieParam; import javax.ws.rs.GET; import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; @@ -94,4 +96,18 @@ public Response responseLocation(@QueryParam("param") String param) throws URISy public Response getCookie() throws SQLException { return Response.ok().cookie(new NewCookie("user-id", "7")).build(); } + + @Path("/api_security/response") + @POST + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + public Response apiSecurityResponse(RequestBody input) { + return Response.ok(input).build(); + } + + @GET + @Path("/api_security/sampling/{i}") + public Response apiSecuritySamplingWithStatus(@PathParam("i") int i) { + return Response.status(i).header("content-type", "text/plain").entity("Hello!\n").build(); + } } diff --git a/dd-smoke-tests/resteasy/src/test/groovy/smoketest/ResteasyAppsecSmokeTest.groovy b/dd-smoke-tests/resteasy/src/test/groovy/smoketest/ResteasyAppsecSmokeTest.groovy new file mode 100644 index 00000000000..abd0c9d9df1 --- /dev/null +++ b/dd-smoke-tests/resteasy/src/test/groovy/smoketest/ResteasyAppsecSmokeTest.groovy @@ -0,0 +1,96 @@ +package smoketest + +import datadog.smoketest.appsec.AbstractAppSecServerSmokeTest +import datadog.trace.api.Platform +import groovy.json.JsonOutput +import groovy.json.JsonSlurper +import okhttp3.MediaType +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.Response + +import java.util.zip.GZIPInputStream + + +class ResteasyAppsecSmokeTest extends AbstractAppSecServerSmokeTest { + + @Override + ProcessBuilder createProcessBuilder() { + String jarPath = System.getProperty("datadog.smoketest.resteasy.jar.path") + + List command = new ArrayList<>() + command.add(javaPath()) + command.addAll(defaultJavaProperties) + command.addAll(defaultAppSecProperties) + if (Platform.isJavaVersionAtLeast(17)) { + command.addAll(["--add-opens", "java.base/java.lang=ALL-UNNAMED"]) + } + command.addAll(["-jar", jarPath, Integer.toString(httpPort)]) + ProcessBuilder processBuilder = new ProcessBuilder(command) + processBuilder.directory(new File(buildDirectory)) + } + + void 'API Security samples only one request per endpoint'() { + given: + def url = "http://localhost:${httpPort}/hello/api_security/sampling/200?test=value" + def request = new Request.Builder() + .url(url) + .addHeader('X-My-Header', "value") + .get() + .build() + + when: + List responses = (1..3).collect { + client.newCall(request).execute() + } + + then: + responses.each { + assert it.code() == 200 + } + waitForTraceCount(3) + def spans = rootSpans.toList().toSorted { it.span.duration } + spans.size() == 3 + def sampledSpans = spans.findAll { + it.meta.keySet().any { + it.startsWith('_dd.appsec.s.req.') + } + } + sampledSpans.size() == 1 + def span = sampledSpans[0] + span.meta.containsKey('_dd.appsec.s.req.query') + span.meta.containsKey('_dd.appsec.s.req.params') + span.meta.containsKey('_dd.appsec.s.req.headers') + } + + + void 'test response schema extraction'() { + given: + def url = "http://localhost:${httpPort}/hello/api_security/response" + def body = [ + "main" : [["key": "id001", "value": 1345.67], ["value": 1567.89, "key": "id002"]], + "nullable": null, + ] + def request = new Request.Builder() + .url(url) + .post(RequestBody.create(MediaType.get('application/json'), JsonOutput.toJson(body))) + .build() + + when: + final response = client.newCall(request).execute() + waitForTraceCount(1) + + then: + response.code() == 200 + def span = rootSpans.first() + span.meta.containsKey('_dd.appsec.s.res.headers') + span.meta.containsKey('_dd.appsec.s.res.body') + final schema = new JsonSlurper().parse(unzip(span.meta.get('_dd.appsec.s.res.body'))) + assert schema == [["main": [[[["key": [8], "value": [16]]]], ["len": 2]], "nullable": [1]]] + } + + private static byte[] unzip(final String text) { + final inflaterStream = new GZIPInputStream(new ByteArrayInputStream(text.decodeBase64())) + return inflaterStream.getBytes() + } +}