From 84a6aa533f9864d0fa5f091fc0bc2c0fd04d51dd Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Fri, 20 Jun 2025 11:54:53 +0200 Subject: [PATCH 1/3] WIP --- dd-smoke-tests/resteasy/build.gradle | 2 + .../src/main/java/smoketest/resteasy/App.java | 4 + .../java/smoketest/resteasy/RequestBody.java | 45 ++++++++ .../java/smoketest/resteasy/Resource.java | 16 +++ .../smoketest/ResteasyAppsecSmokeTest.groovy | 101 ++++++++++++++++++ 5 files changed, 168 insertions(+) create mode 100644 dd-smoke-tests/resteasy/src/main/java/smoketest/resteasy/RequestBody.java create mode 100644 dd-smoke-tests/resteasy/src/test/groovy/smoketest/ResteasyAppsecSmokeTest.groovy diff --git a/dd-smoke-tests/resteasy/build.gradle b/dd-smoke-tests/resteasy/build.gradle index 089d9a44052..66e8b57dbcd 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 'org.jboss.resteasy:resteasy-jackson2-provider: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..6fd0df98ef2 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 bodyJson(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..98609841d80 --- /dev/null +++ b/dd-smoke-tests/resteasy/src/test/groovy/smoketest/ResteasyAppsecSmokeTest.groovy @@ -0,0 +1,101 @@ +package smoketest + +import datadog.smoketest.appsec.AbstractAppSecServerSmokeTest +import datadog.trace.agent.test.utils.OkHttpUtils +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 spock.lang.IgnoreIf + +import java.util.zip.GZIPInputStream + +@IgnoreIf({ + System.getProperty("java.vendor").contains("IBM") && System.getProperty("java.version").contains("1.8.") +}) +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 client = OkHttpUtils.clientBuilder().build() + 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() + } +} From 9e0f4df657b50a69f791602cd43687c61aed20db Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Wed, 2 Jul 2025 15:32:40 +0200 Subject: [PATCH 2/3] PR reviews --- dd-smoke-tests/resteasy/build.gradle | 2 +- .../resteasy/src/main/java/smoketest/resteasy/Resource.java | 2 +- .../src/test/groovy/smoketest/ResteasyAppsecSmokeTest.groovy | 5 +---- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/dd-smoke-tests/resteasy/build.gradle b/dd-smoke-tests/resteasy/build.gradle index 66e8b57dbcd..5b7fd20deda 100644 --- a/dd-smoke-tests/resteasy/build.gradle +++ b/dd-smoke-tests/resteasy/build.gradle @@ -16,7 +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 'org.jboss.resteasy:resteasy-jackson2-provider:3.1.0.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' 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 6fd0df98ef2..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 @@ -101,7 +101,7 @@ public Response getCookie() throws SQLException { @POST @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - public Response bodyJson(RequestBody input) { + public Response apiSecurityResponse(RequestBody input) { return Response.ok(input).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 index 98609841d80..6d9f4c256ef 100644 --- a/dd-smoke-tests/resteasy/src/test/groovy/smoketest/ResteasyAppsecSmokeTest.groovy +++ b/dd-smoke-tests/resteasy/src/test/groovy/smoketest/ResteasyAppsecSmokeTest.groovy @@ -13,9 +13,7 @@ import spock.lang.IgnoreIf import java.util.zip.GZIPInputStream -@IgnoreIf({ - System.getProperty("java.vendor").contains("IBM") && System.getProperty("java.version").contains("1.8.") -}) + class ResteasyAppsecSmokeTest extends AbstractAppSecServerSmokeTest { @Override @@ -71,7 +69,6 @@ class ResteasyAppsecSmokeTest extends AbstractAppSecServerSmokeTest { void 'test response schema extraction'() { given: def url = "http://localhost:${httpPort}/hello/api_security/response" - def client = OkHttpUtils.clientBuilder().build() def body = [ "main" : [["key": "id001", "value": 1345.67], ["value": 1567.89, "key": "id002"]], "nullable": null, From a511ca037f536bce315865ab9df972df20d2ed47 Mon Sep 17 00:00:00 2001 From: "alejandro.gonzalez" Date: Thu, 3 Jul 2025 08:03:20 +0200 Subject: [PATCH 3/3] fix codenarc --- .../src/test/groovy/smoketest/ResteasyAppsecSmokeTest.groovy | 2 -- 1 file changed, 2 deletions(-) diff --git a/dd-smoke-tests/resteasy/src/test/groovy/smoketest/ResteasyAppsecSmokeTest.groovy b/dd-smoke-tests/resteasy/src/test/groovy/smoketest/ResteasyAppsecSmokeTest.groovy index 6d9f4c256ef..abd0c9d9df1 100644 --- a/dd-smoke-tests/resteasy/src/test/groovy/smoketest/ResteasyAppsecSmokeTest.groovy +++ b/dd-smoke-tests/resteasy/src/test/groovy/smoketest/ResteasyAppsecSmokeTest.groovy @@ -1,7 +1,6 @@ package smoketest import datadog.smoketest.appsec.AbstractAppSecServerSmokeTest -import datadog.trace.agent.test.utils.OkHttpUtils import datadog.trace.api.Platform import groovy.json.JsonOutput import groovy.json.JsonSlurper @@ -9,7 +8,6 @@ import okhttp3.MediaType import okhttp3.Request import okhttp3.RequestBody import okhttp3.Response -import spock.lang.IgnoreIf import java.util.zip.GZIPInputStream