From 6a70c29c92d7be63c81f89795283057cf1228558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20=C3=81lvarez=20=C3=81lvarez?= Date: Thu, 18 May 2023 12:50:45 +0200 Subject: [PATCH] Add utilities to simplify IAST smoke testing --- .../jersey/CookieInstrumentation.java | 2 +- .../jersey/ThreadLocalSourceType.java | 2 +- .../AbstractIastServerSmokeTest.groovy | 147 +++++- .../AbstractIastSpringBootTest.groovy | 436 +++++++++++++++++ .../smoketest/AbstractJerseySmokeTest.groovy | 252 ++++++++++ .../smoketest/model/Vulnerability.groovy | 33 ++ dd-smoke-tests/jersey-2/build.gradle | 3 +- .../src/main/java/com/restserver/DB.java | 32 -- .../main/java/com/restserver/Resource.java | 69 +-- .../datadog/smoketest/Jersey2SmokeTest.groovy | 284 +---------- dd-smoke-tests/jersey-3/build.gradle | 3 +- .../jersey-3/src/main/java/smoketest/DB.java | 31 -- .../src/main/java/smoketest/Resource.java | 53 +- .../datadog/smoketest/Jersey3SmokeTest.groovy | 299 +----------- dd-smoke-tests/resteasy/build.gradle | 3 +- .../src/main/java/smoketest/resteasy/DB.java | 32 -- .../java/smoketest/resteasy/Resource.java | 24 +- .../groovy/smoketest/ResteasySmokeTest.groovy | 85 ++-- .../spring-boot-2.6-webmvc/build.gradle | 2 + .../springboot/SpringbootApplication.java | 33 ++ .../controller/IastWebController.java | 56 +++ .../controller/SimpleIastController.java | 24 + .../groovy/IastSpringBootSmokeTest.groovy | 457 +----------------- dd-smoke-tests/spring-security/build.gradle | 4 + .../smoketest/SpringSecurityJwtTest.groovy | 36 +- dd-smoke-tests/springboot/build.gradle | 3 + .../controller/IastWebController.java | 7 +- .../AbstractSpringBootIastTest.groovy | 110 ----- .../IastSpringBootRedirectSmokeTest.groovy | 25 +- .../smoketest/IastSpringBootSmokeTest.groovy | 431 +---------------- .../smoketest/IastVertxSmokeTest.groovy | 2 +- .../smoketest/IastVertxSmokeTest.groovy | 2 +- 32 files changed, 1145 insertions(+), 1837 deletions(-) create mode 100644 dd-smoke-tests/iast-util/src/testFixtures/groovy/datadog/smoketest/AbstractIastSpringBootTest.groovy create mode 100644 dd-smoke-tests/iast-util/src/testFixtures/groovy/datadog/smoketest/AbstractJerseySmokeTest.groovy create mode 100644 dd-smoke-tests/iast-util/src/testFixtures/groovy/datadog/smoketest/model/Vulnerability.groovy delete mode 100644 dd-smoke-tests/jersey-2/src/main/java/com/restserver/DB.java delete mode 100644 dd-smoke-tests/jersey-3/src/main/java/smoketest/DB.java delete mode 100644 dd-smoke-tests/resteasy/src/main/java/smoketest/resteasy/DB.java create mode 100644 dd-smoke-tests/spring-boot-2.6-webmvc/src/main/java/datadog/smoketest/springboot/controller/SimpleIastController.java delete mode 100644 dd-smoke-tests/springboot/src/test/groovy/datadog/smoketest/AbstractSpringBootIastTest.groovy diff --git a/dd-java-agent/instrumentation/jersey/src/main/java/datadog/trace/instrumentation/jersey/CookieInstrumentation.java b/dd-java-agent/instrumentation/jersey/src/main/java/datadog/trace/instrumentation/jersey/CookieInstrumentation.java index 9167a52d26d..c2e637c4897 100644 --- a/dd-java-agent/instrumentation/jersey/src/main/java/datadog/trace/instrumentation/jersey/CookieInstrumentation.java +++ b/dd-java-agent/instrumentation/jersey/src/main/java/datadog/trace/instrumentation/jersey/CookieInstrumentation.java @@ -52,7 +52,7 @@ public static void onExit( @Advice.This Object self) { final PropagationModule module = InstrumentationBridge.PROPAGATION; if (module != null) { - module.taintIfInputIsTainted(SourceTypes.REQUEST_COOKIE_NAME, name, cookieValue, self); + module.taintIfInputIsTainted(SourceTypes.REQUEST_COOKIE_VALUE, name, cookieValue, self); } } } diff --git a/dd-java-agent/instrumentation/jersey/src/main/java/datadog/trace/instrumentation/jersey/ThreadLocalSourceType.java b/dd-java-agent/instrumentation/jersey/src/main/java/datadog/trace/instrumentation/jersey/ThreadLocalSourceType.java index c203235b6c0..299f2e9bfc6 100644 --- a/dd-java-agent/instrumentation/jersey/src/main/java/datadog/trace/instrumentation/jersey/ThreadLocalSourceType.java +++ b/dd-java-agent/instrumentation/jersey/src/main/java/datadog/trace/instrumentation/jersey/ThreadLocalSourceType.java @@ -6,7 +6,7 @@ public class ThreadLocalSourceType { private static final ThreadLocal SOURCE = ThreadLocal.withInitial(() -> SourceTypes.REQUEST_PARAMETER_VALUE); - public static void set(Byte source) { + public static void set(byte source) { SOURCE.set(source); } diff --git a/dd-smoke-tests/iast-util/src/testFixtures/groovy/datadog/smoketest/AbstractIastServerSmokeTest.groovy b/dd-smoke-tests/iast-util/src/testFixtures/groovy/datadog/smoketest/AbstractIastServerSmokeTest.groovy index d42d6deb298..3d02cbafa67 100644 --- a/dd-smoke-tests/iast-util/src/testFixtures/groovy/datadog/smoketest/AbstractIastServerSmokeTest.groovy +++ b/dd-smoke-tests/iast-util/src/testFixtures/groovy/datadog/smoketest/AbstractIastServerSmokeTest.groovy @@ -1,15 +1,23 @@ package datadog.smoketest import datadog.smoketest.model.TaintedObject +import datadog.smoketest.model.Vulnerability +import groovy.json.JsonBuilder import groovy.json.JsonSlurper import groovy.transform.CompileDynamic +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.SimpleType +import org.spockframework.runtime.SpockTimeoutError import spock.lang.Shared +import spock.util.concurrent.PollingConditions -import java.util.function.Predicate +import java.util.concurrent.TimeoutException @CompileDynamic abstract class AbstractIastServerSmokeTest extends AbstractServerSmokeTest { + private static final String TAG_NAME = '_dd.iast.json' + @Shared private final JsonSlurper jsonSlurper = new JsonSlurper() @@ -18,18 +26,139 @@ abstract class AbstractIastServerSmokeTest extends AbstractServerSmokeTest { return 'debug' } - protected void hasTainted(final Predicate matcher) { - processTestLogLines { String log -> - final index = log.indexOf('tainted=') - if (index >= 0) { - final tainted = jsonSlurper.parse(new StringReader(log.substring(index + 8))) as TaintedObject - return matcher.test(tainted) + @Override + Closure decodedTracesCallback() { + return {} // force traces decoding + } + + protected static String withSystemProperty(final String config, final Object value) { + return "-Ddd.${config}=${value}" + } + + protected void hasTainted(@ClosureParams(value = SimpleType, options = ['datadog.smoketest.model.TaintedObject']) + final Closure matcher) { + boolean found = false + final tainteds = [] + final closure = { String log -> + final tainted = parseTaintedLog(log) + if (tainted != null) { + tainteds.add(tainted) + if (matcher.call(tainted)) { + found = true + return true // found + } } return false } + try { + processTestLogLines(closure) + } catch (TimeoutException toe) { + checkLogPostExit(closure) + if (!found) { + throw new AssertionError("No matching tainted found. Tainteds found: ${new JsonBuilder(tainteds).toPrettyString()}") + } + } } - protected static String withSystemProperty(final String config, final Object value) { - return "-Ddd.${config}=${value}" + /** + * Use {@link #hasVulnerability(groovy.lang.Closure)} if possible + */ + @Deprecated + protected void hasVulnerabilityInLogs(@ClosureParams(value = SimpleType, options = ['datadog.smoketest.model.Vulnerability']) + final Closure matcher) { + boolean found = false + final vulnerabilities = [] + final closure = { String log -> + final parsed = parseVulnerabilitiesLog(log) + if (parsed != null) { + vulnerabilities.addAll(parsed) + if (parsed.find(matcher) != null) { + found = true + return true + } + } + return false + } + try { + processTestLogLines(closure) + } catch (TimeoutException toe) { + checkLogPostExit(closure) + if (!found) { + throw new AssertionError("No matching vulnerability found. Vulnerabilities found: ${new JsonBuilder(discovered).toPrettyString()}") + } + } + } + + protected void hasMetric(final String name, final Object value) { + final found = [] + try { + waitForSpan(pollingConditions()) { span -> + if (span.metrics.containsKey(name)) { + final metric = span.metrics.get(name) + found.add(metric) + if (metric == value) { + return true + } + } + return false + } + } catch (SpockTimeoutError toe) { + throw new AssertionError("No matching metric with name $name found. Metrics found: ${new JsonBuilder(found).toPrettyString()}") + } + } + + protected void hasVulnerability(@ClosureParams(value = SimpleType, options = ['datadog.smoketest.model.Vulnerability']) + final Closure matcher) { + final found = [] + try { + waitForSpan(pollingConditions()) { span -> + final json = span.meta.get(TAG_NAME) + if (!json) { + return false + } + final batch = jsonSlurper.parseText(json) as Map + final vulnerabilities = batch.vulnerabilities as List + found.addAll(vulnerabilities) + return vulnerabilities.find(matcher) != null + } + } catch (SpockTimeoutError toe) { + throw new AssertionError("No matching vulnerability found. Vulnerabilities found: ${new JsonBuilder(found).toPrettyString()}") + } + } + + protected TaintedObject parseTaintedLog(final String log) { + final index = log.indexOf('tainted=') + if (index >= 0) { + return jsonSlurper.parse(new StringReader(log.substring(index + 8))) as TaintedObject + } + return null + } + + protected List parseVulnerabilitiesLog(final String log) { + final startIndex = log.indexOf(TAG_NAME) + if (startIndex < 0) { + return null + } + final chars = log.toCharArray() + final builder = new StringBuilder() + def level = 0 + for (int i = log.indexOf('{', startIndex); i < chars.length; i++) { + final current = chars[i] + if (current == '{' as char) { + level++ + } else if (current == '}' as char) { + level-- + } + builder.append(chars[i]) + if (level == 0) { + break + } + } + final batch = jsonSlurper.parseText(builder.toString()) as Map + return batch.vulnerabilities as List + } + + protected PollingConditions pollingConditions() { + return new PollingConditions(timeout: 5) } } diff --git a/dd-smoke-tests/iast-util/src/testFixtures/groovy/datadog/smoketest/AbstractIastSpringBootTest.groovy b/dd-smoke-tests/iast-util/src/testFixtures/groovy/datadog/smoketest/AbstractIastSpringBootTest.groovy new file mode 100644 index 00000000000..bf41a634861 --- /dev/null +++ b/dd-smoke-tests/iast-util/src/testFixtures/groovy/datadog/smoketest/AbstractIastSpringBootTest.groovy @@ -0,0 +1,436 @@ +package datadog.smoketest + +import okhttp3.FormBody +import okhttp3.MediaType +import okhttp3.Request +import okhttp3.RequestBody +import spock.lang.Ignore + +import static datadog.trace.api.config.IastConfig.IAST_DEBUG_ENABLED +import static datadog.trace.api.config.IastConfig.IAST_DETECTION_MODE +import static datadog.trace.api.config.IastConfig.IAST_ENABLED +import static datadog.trace.api.config.IastConfig.IAST_REDACTION_ENABLED + +abstract class AbstractIastSpringBootTest extends AbstractIastServerSmokeTest { + + private static final MediaType JSON = MediaType.parse('application/json; charset=utf-8') + + @Override + ProcessBuilder createProcessBuilder() { + String springBootShadowJar = System.getProperty('datadog.smoketest.springboot.shadowJar.path') + + List command = [] + command.add(javaPath()) + command.addAll(defaultJavaProperties) + command.addAll([ + withSystemProperty(IAST_ENABLED, true), + withSystemProperty(IAST_DETECTION_MODE, 'FULL'), + withSystemProperty(IAST_DEBUG_ENABLED, true), + withSystemProperty(IAST_REDACTION_ENABLED, false) + ]) + command.addAll((String[]) ['-jar', springBootShadowJar, "--server.port=${httpPort}"]) + ProcessBuilder processBuilder = new ProcessBuilder(command) + processBuilder.directory(new File(buildDirectory)) + // Spring will print all environment variables to the log, which may pollute it and affect log assertions. + processBuilder.environment().clear() + return processBuilder + } + + void 'IAST subsystem starts'() { + given: 'an initial request has succeeded' + String url = "http://localhost:${httpPort}/greeting" + def request = new Request.Builder().url(url).get().build() + client.newCall(request).execute() + + when: 'logs are read' + String startMsg = null + String errorMsg = null + checkLogPostExit { + if (it.contains('Not starting IAST subsystem')) { + errorMsg = it + } + if (it.contains('IAST is starting')) { + startMsg = it + } + // Check that there's no logged exception about missing classes from Datadog. + // We had this problem before with JDK9StackWalker. + if (it.contains('java.lang.ClassNotFoundException: datadog/')) { + errorMsg = it + } + } + + then: 'there are no errors in the log and IAST has started' + errorMsg == null + startMsg != null + !logHasErrors + } + + void 'default home page without errors'() { + setup: + String url = "http://localhost:${httpPort}/greeting" + def request = new Request.Builder().url(url).get().build() + + when: + def response = client.newCall(request).execute() + + then: + def responseBodyStr = response.body().string() + responseBodyStr != null + responseBodyStr.contains('Sup Dawg') + response.body().contentType().toString().contains('text/plain') + response.code() == 200 + + checkLogPostExit() + !logHasErrors + } + + void 'iast.enabled tag is present'() { + setup: + String url = "http://localhost:${httpPort}/greeting" + def request = new Request.Builder().url(url).get().build() + + when: + client.newCall(request).execute() + + then: + hasMetric('_dd.iast.enabled', 1) + } + + void 'weak hash vulnerability is present'() { + setup: + String url = "http://localhost:${httpPort}/weakhash" + def request = new Request.Builder().url(url).get().build() + + when: + client.newCall(request).execute() + + then: + hasVulnerability { vul -> + vul.type == 'WEAK_HASH' && + vul.evidence.value == 'MD5' + } + } + + void 'insecure cookie vulnerability is present'() { + setup: + String url = "http://localhost:${httpPort}/insecure_cookie" + def request = new Request.Builder().url(url).get().build() + + when: + def response = client.newCall(request).execute() + + then: + response.isSuccessful() + response.header('Set-Cookie').contains('user-id') + hasVulnerability { vul -> + vul.type == 'INSECURE_COOKIE' && + vul.evidence.value == 'user-id' + } + } + + void 'insecure cookie vulnerability from addheader is present'() { + setup: + String url = "http://localhost:${httpPort}/insecure_cookie_from_header" + def request = new Request.Builder().url(url).get().build() + + when: + def response = client.newCall(request).execute() + + then: + response.isSuccessful() + response.header('Set-Cookie').contains('user-id') + hasVulnerability { vul -> + vul.type == 'INSECURE_COOKIE' && + vul.evidence.value == 'user-id' + } + } + + + void 'weak hash vulnerability is present on boot'() { + setup: + String url = "http://localhost:${httpPort}/greeting" + def request = new Request.Builder().url(url).get().build() + + when: 'ensure the controller is loaded' + client.newCall(request).execute() + + then: 'a vulnerability pops in the logs (startup traces might not always be available)' + hasVulnerabilityInLogs { vul -> + vul.type == 'WEAK_HASH' && + vul.evidence.value == 'SHA1' && + vul.location.spanId > 0 + } + } + + void 'weak hash vulnerability is present on thread'() { + setup: + String url = "http://localhost:${httpPort}/async_weakhash" + def request = new Request.Builder().url(url).get().build() + + when: + client.newCall(request).execute() + + then: + hasVulnerability { vul -> + vul.type == 'WEAK_HASH' && + vul.evidence.value == 'MD4' && + vul.location.spanId > 0 + } + } + + void 'getParameter taints string'() { + setup: + String url = "http://localhost:${httpPort}/getparameter?param=A" + def request = new Request.Builder().url(url).get().build() + + when: + client.newCall(request).execute() + + then: + hasTainted { tainted -> + tainted.value == 'A' && + tainted.ranges[0].source.name == 'param' && + tainted.ranges[0].source.origin == 'http.request.parameter' + } + } + + void 'command injection is present with runtime'() { + setup: + final url = "http://localhost:${httpPort}/cmdi/runtime?cmd=ls" + final request = new Request.Builder().url(url).get().build() + + when: + client.newCall(request).execute() + + then: + hasVulnerability { vul -> vul.type == 'COMMAND_INJECTION' } + } + + void 'command injection is present with process builder'() { + setup: + final url = "http://localhost:${httpPort}/cmdi/process_builder?cmd=ls" + final request = new Request.Builder().url(url).get().build() + + when: + client.newCall(request).execute() + + then: + hasVulnerability { vul -> vul.type == 'COMMAND_INJECTION' } + } + + void 'path traversal is present with file'() { + setup: + final url = "http://localhost:${httpPort}/path_traversal/file?path=test" + final request = new Request.Builder().url(url).get().build() + + when: + client.newCall(request).execute() + + then: + hasVulnerability { vul -> vul.type == 'PATH_TRAVERSAL' } + } + + void 'path traversal is present with paths'() { + setup: + final url = "http://localhost:${httpPort}/path_traversal/paths?path=test" + final request = new Request.Builder().url(url).get().build() + + when: + client.newCall(request).execute() + + then: + hasVulnerability { vul -> vul.type == 'PATH_TRAVERSAL' } + } + + void 'path traversal is present with path'() { + setup: + final url = "http://localhost:${httpPort}/path_traversal/path?path=test" + final request = new Request.Builder().url(url).get().build() + + when: + client.newCall(request).execute() + + then: + hasVulnerability { vul -> vul.type == 'PATH_TRAVERSAL' } + } + + void 'parameter binding taints bean strings'() { + setup: + String url = "http://localhost:${httpPort}/param_binding/test?name=parameter&value=binding" + def request = new Request.Builder().url(url).get().build() + + when: + client.newCall(request).execute() + + then: + hasTainted { tainted -> + tainted.value == 'binding' && + tainted.ranges[0].source.name == 'value' && + tainted.ranges[0].source.origin == 'http.request.parameter' + } + } + + void 'request header taint string'() { + setup: + String url = "http://localhost:${httpPort}/request_header/test" + def request = new Request.Builder().url(url).header("test-header", "test").get().build() + + when: + client.newCall(request).execute() + + then: + hasTainted { tainted -> + tainted.value == 'test' && + tainted.ranges[0].source.name == 'test-header' && + tainted.ranges[0].source.origin == 'http.request.header' + } + } + + void 'path param taint string'() { + setup: + String url = "http://localhost:${httpPort}/path_param?param=test" + def request = new Request.Builder().url(url).get().build() + + when: + client.newCall(request).execute() + + then: + hasTainted { tainted -> + tainted.value == 'test' && + tainted.ranges[0].source.name == 'param' && + tainted.ranges[0].source.origin == 'http.request.parameter' + } + } + + void 'request body taint json'() { + setup: + String url = "http://localhost:${httpPort}/request_body/test" + def request = new Request.Builder().url(url).post(RequestBody.create(JSON, '{"name": "nameTest", "value" : "valueTest"}')).build() + + when: + client.newCall(request).execute() + + then: + hasTainted { tainted -> + tainted.value == 'nameTest' && + tainted.ranges[0].source.origin == 'http.request.body' + } + } + + void 'request query string'() { + given: + final url = "http://localhost:${httpPort}/query_string?key=value" + final request = new Request.Builder().url(url).get().build() + + when: + client.newCall(request).execute() + + then: + hasTainted { tainted -> + tainted.value == 'key=value' && + tainted.ranges[0].source.origin == 'http.request.query' + } + } + + void 'request cookie propagation'() { + given: + final url = "http://localhost:${httpPort}/cookie" + final request = new Request.Builder().url(url).header('Cookie', 'name=value').get().build() + + when: + client.newCall(request).execute() + + then: + hasTainted { tainted -> + tainted.value == 'name' && + tainted.ranges[0].source.origin == 'http.request.cookie.name' + } + hasTainted { tainted -> + tainted.value == 'value' && + tainted.ranges[0].source.name == 'name' && + tainted.ranges[0].source.origin == 'http.request.cookie.value' + } + } + + void 'tainting of path variables — simple variant'() { + given: + String url = "http://localhost:${httpPort}/simple/foobar" + def request = new Request.Builder().url(url).get().build() + + when: + client.newCall(request).execute() + + then: + hasTainted { + it.value == 'foobar' && + it.ranges[0].source.origin == 'http.request.path.parameter' && + it.ranges[0].source.name == 'var1' + } + } + + // TODO @IAST support this source in spring boot 2.6 + @Ignore + @SuppressWarnings('CyclomaticComplexity') + void 'tainting of path variables — RequestMappingInfoHandlerMapping variant'() { + given: + String url = "http://localhost:${httpPort}/matrix/value;xxx=aaa,bbb;yyy=ccc/zzz=ddd" + def request = new Request.Builder().url(url).get().build() + + when: + client.newCall(request).execute() + + then: + hasTainted { tainted -> + final firstRange = tainted.ranges[0] + tainted.value == 'value' && + firstRange?.source?.origin == 'http.request.path.parameter' && + firstRange?.source?.name == 'var1' + } + ['xxx', 'aaa', 'bbb', 'yyy', 'ccc'].each { + hasTainted { tainted -> + final firstRange = tainted.ranges[0] + firstRange?.source?.origin == 'http.request.matrix.parameter' && + firstRange?.source?.name == 'var1' + } + } + hasTainted { tainted -> + final firstRange = tainted.ranges[0] + tainted.value == 'zzz=ddd' && + firstRange?.source?.origin == 'http.request.path.parameter' && + firstRange?.source?.name == 'var2' + } + ['zzz', 'ddd'].each { + hasTainted { tainted -> + final firstRange = tainted.ranges[0] + tainted.value = it && + firstRange?.source?.origin == 'http.request.matrix.parameter' && + firstRange?.source?.name == 'var2' + } + } + } + + void 'ssrf is present'() { + setup: + final url = "http://localhost:${httpPort}/ssrf" + final body = new FormBody.Builder().add('url', 'https://dd.datad0g.com/').build() + final request = new Request.Builder().url(url).post(body).build() + + when: + client.newCall(request).execute() + + then: + hasVulnerability { vul -> vul.type == 'SSRF' } + } + + void 'test iast metrics stored in spans'() { + setup: + final url = "http://localhost:${httpPort}/cmdi/runtime?cmd=ls" + final request = new Request.Builder().url(url).get().build() + + when: + client.newCall(request).execute() + + then: + hasMetric('_dd.iast.telemetry.executed.sink.command_injection', 1) + } +} diff --git a/dd-smoke-tests/iast-util/src/testFixtures/groovy/datadog/smoketest/AbstractJerseySmokeTest.groovy b/dd-smoke-tests/iast-util/src/testFixtures/groovy/datadog/smoketest/AbstractJerseySmokeTest.groovy new file mode 100644 index 00000000000..091e5a60f4d --- /dev/null +++ b/dd-smoke-tests/iast-util/src/testFixtures/groovy/datadog/smoketest/AbstractJerseySmokeTest.groovy @@ -0,0 +1,252 @@ +package datadog.smoketest + +import okhttp3.FormBody +import okhttp3.Request +import spock.lang.Ignore + +class AbstractJerseySmokeTest extends AbstractIastServerSmokeTest { + + // TODO @IAST support this source in jersey 2 and 3 + @Ignore + void 'path parameter'() { + setup: + def url = "http://localhost:${httpPort}/hello/bypathparam/pathParamValue" + + when: + def request = new Request.Builder().url(url).get().build() + def response = client.newCall(request).execute() + + then: + String body = response.body().string() + assert body != null + assert response.body().contentType().toString().contains("text/plain") + assert body.contains("Jersey: hello pathParamValue") + assert response.code() == 200 + hasTainted { tainted -> + tainted.value == 'pathParamValue' && + tainted.ranges[0].source.origin == 'http.request.path.parameter' + } + } + + void 'query parameter'() { + setup: + def url = "http://localhost:${httpPort}/hello/byqueryparam?param=queryParamValue" + + when: + def request = new Request.Builder().url(url).get().build() + def response = client.newCall(request).execute() + + then: + String body = response.body().string() + assert body != null + assert response.body().contentType().toString().contains("text/plain") + assert body.contains("Jersey: hello queryParamValue") + assert response.code() == 200 + hasTainted { tainted -> + tainted.value == 'queryParamValue' && + tainted.ranges[0].source.name == 'param' && + tainted.ranges[0].source.origin == 'http.request.parameter' + } + } + + + void 'header'() { + setup: + def url = "http://localhost:${httpPort}/hello/byheader" + + when: + def request = new Request.Builder().url(url).header("X-Custom-header", "pepito").get().build() + def response = client.newCall(request).execute() + + then: + String body = response.body().string() + assert body != null + assert response.body().contentType().toString().contains("text/plain") + assert body.contains("Jersey: hello pepito") + assert response.code() == 200 + hasTainted { tainted -> + tainted.value == 'pepito' && + tainted.ranges[0].source.name == 'X-Custom-header' && + tainted.ranges[0].source.origin == 'http.request.header' + } + } + + void 'header name'() { + setup: + def url = "http://localhost:${httpPort}/hello/headername" + + when: + def request = new Request.Builder().url(url).header("X-Custom-header", "pepito").get().build() + def response = client.newCall(request).execute() + + then: + String body = response.body().string() + assert body != null + assert response.body().contentType().toString().contains("text/plain") + assert body.equalsIgnoreCase("Jersey: hello X-Custom-header") + assert response.code() == 200 + hasTainted { tainted -> + tainted.value.equalsIgnoreCase('X-Custom-header') && + tainted.ranges[0].source.origin == 'http.request.header.name' + } + } + + void 'cookie'() { + setup: + def url = "http://localhost:${httpPort}/hello/bycookie" + + when: + def request = new Request.Builder().url(url).addHeader("Cookie", "cookieName=cookieValue").get().build() + def response = client.newCall(request).execute() + + then: + String body = response.body().string() + assert body != null + assert response.body().contentType().toString().contains("text/plain") + assert body.equalsIgnoreCase("Jersey: hello cookieValue") + assert response.code() == 200 + hasTainted { tainted -> + tainted.value == 'cookieValue' && + tainted.ranges[0].source.name == 'cookieName' && + tainted.ranges[0].source.origin == 'http.request.cookie.value' + } + } + + void 'unvalidated redirect from location header is present'() { + setup: + def url = "http://localhost:${httpPort}/hello/setlocationheader?param=queryParamValue" + + when: + def request = new Request.Builder().url(url).get().build() + def response = client.newCall(request).execute() + + then: + response.isRedirect() + hasVulnerability { vul -> vul.type == 'UNVALIDATED_REDIRECT' } + } + + void 'unvalidated redirect from location is present'() { + setup: + def url = "http://localhost:${httpPort}/hello/setresponselocation?param=queryParamValue" + + when: + def request = new Request.Builder().url(url).get().build() + def response = client.newCall(request).execute() + + then: + response.isRedirect() + hasVulnerability { vul -> vul.type == 'UNVALIDATED_REDIRECT' } + } + + void 'cookie name from Cookie object'() { + setup: + def url = "http://localhost:${httpPort}/hello/cookiename" + + when: + def request = new Request.Builder().url(url).addHeader("Cookie", "cookieName=cookieValue").get().build() + def response = client.newCall(request).execute() + + then: + String body = response.body().string() + assert body != null + assert response.body().contentType().toString().contains("text/plain") + assert body.contains("Jersey: hello cookieName") + assert response.code() == 200 + hasTainted { tainted -> + tainted.value == 'cookieName' && + tainted.ranges[0].source.origin == 'http.request.cookie.name' + } + } + + void 'cookie value from Cookie object'() { + setup: + def url = "http://localhost:${httpPort}/hello/cookieobjectvalue" + + when: + def request = new Request.Builder().url(url).addHeader("Cookie", "cookieName=cookieObjectValue").get().build() + def response = client.newCall(request).execute() + + then: + String body = response.body().string() + assert body != null + assert response.body().contentType().toString().contains("text/plain") + assert body.contains("Jersey: hello cookieObjectValue") + assert response.code() == 200 + hasTainted { tainted -> + tainted.value == 'cookieObjectValue' && + tainted.ranges[0].source.name == 'cookieName' && + tainted.ranges[0].source.origin == 'http.request.cookie.value' + } + } + + void 'form parameter values'() { + setup: + def url = "http://localhost:${httpPort}/hello/formparameter" + + when: + def formBody = new FormBody.Builder() + formBody.add("formParam1Name", "formParam1Value") + def request = new Request.Builder().url(url).post(formBody.build()).build() + def response = client.newCall(request).execute() + + then: + String body = response.body().string() + assert body != null + assert response.body().contentType().toString().contains("text/plain") + assert body.contains("Jersey: hello formParam1Value") + assert response.code() == 200 + hasTainted { tainted -> + tainted.value == 'formParam1Value' && + tainted.ranges[0].source.name == 'formParam1Name' && + tainted.ranges[0].source.origin == 'http.request.parameter' + } + } + + void 'form parameter name'() { + setup: + def url = "http://localhost:${httpPort}/hello/formparametername" + + when: + def formBody = new FormBody.Builder() + formBody.add("formParam1Name", "formParam1Value") + def request = new Request.Builder().url(url).post(formBody.build()).build() + def response = client.newCall(request).execute() + + then: + String body = response.body().string() + assert body != null + assert response.body().contentType().toString().contains("text/plain") + assert body.contains("Jersey: hello formParam1Name") + assert response.code() == 200 + hasTainted { tainted -> + tainted.value == 'formParam1Name' && + tainted.ranges[0].source.origin == 'http.request.parameter.name' + } + } + + void 'unvalidated redirect from location header is present'() { + setup: + def url = "http://localhost:${httpPort}/hello/setlocationheader?param=queryParamValue" + + when: + def request = new Request.Builder().url(url).get().build() + def response = client.newCall(request).execute() + + then: + response.isRedirect() + hasVulnerability { vul -> vul.type == 'UNVALIDATED_REDIRECT' } + } + + void 'unvalidated redirect from location is present'() { + setup: + def url = "http://localhost:${httpPort}/hello/setresponselocation?param=queryParamValue" + + when: + def request = new Request.Builder().url(url).get().build() + def response = client.newCall(request).execute() + + then: + response.isRedirect() + hasVulnerability { vul -> vul.type == 'UNVALIDATED_REDIRECT' } + } +} diff --git a/dd-smoke-tests/iast-util/src/testFixtures/groovy/datadog/smoketest/model/Vulnerability.groovy b/dd-smoke-tests/iast-util/src/testFixtures/groovy/datadog/smoketest/model/Vulnerability.groovy new file mode 100644 index 00000000000..a50a91d499e --- /dev/null +++ b/dd-smoke-tests/iast-util/src/testFixtures/groovy/datadog/smoketest/model/Vulnerability.groovy @@ -0,0 +1,33 @@ +package datadog.smoketest.model + +import groovy.transform.CompileStatic +import groovy.transform.ToString + +@CompileStatic +@ToString +class Vulnerability { + + String type + Evidence evidence + Location location + + @ToString + class Evidence { + String value + List valueParts + } + + @ToString + class ValuePart { + String value + int source + } + + @ToString + class Location { + String path + int line + String method + Long spanId + } +} diff --git a/dd-smoke-tests/jersey-2/build.gradle b/dd-smoke-tests/jersey-2/build.gradle index d8a562412ee..1b2536dea91 100644 --- a/dd-smoke-tests/jersey-2/build.gradle +++ b/dd-smoke-tests/jersey-2/build.gradle @@ -1,5 +1,6 @@ plugins { id "com.github.johnrengelman.shadow" + id 'java-test-fixtures' } apply from: "$rootDir/gradle/java.gradle" @@ -17,8 +18,8 @@ dependencies { implementation group: 'org.glassfish.jersey.containers', name: 'jersey-container-servlet-core', version:'2.0' implementation group: 'org.glassfish.jersey.media', name: 'jersey-media-json-jackson', version:'2.0' implementation group: 'javax.xml', name: 'jaxb-api', version:'2.1' - implementation group: 'com.h2database', name: 'h2', version: '1.3.148' testImplementation project(':dd-smoke-tests') + testImplementation(testFixtures(project(":dd-smoke-tests:iast-util"))) } tasks.withType(Test).configureEach { diff --git a/dd-smoke-tests/jersey-2/src/main/java/com/restserver/DB.java b/dd-smoke-tests/jersey-2/src/main/java/com/restserver/DB.java deleted file mode 100644 index 4ff621e5a81..00000000000 --- a/dd-smoke-tests/jersey-2/src/main/java/com/restserver/DB.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.restserver; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; - -public class DB { - - public static void main(String[] args) throws SQLException { - DB.store("pepe"); - } - - @SuppressFBWarnings - public static void store(String value) throws SQLException { - try (Connection conn = DriverManager.getConnection("jdbc:h2:mem:test_mem"); - Statement st = conn.createStatement()) { - st.execute("create table pepe (title VARCHAR(50) NOT NULL) "); - st.executeUpdate( - new StringBuilder("insert into pepe values('").append(value).append("')").toString()); - System.out.println("Inserted value " + value); - try (ResultSet rs = st.executeQuery("select * from pepe")) { - rs.next(); - if (!rs.getString(1).equals(value)) { - throw new SQLException("Value " + value + " not found in db"); - } - } - } - } -} diff --git a/dd-smoke-tests/jersey-2/src/main/java/com/restserver/Resource.java b/dd-smoke-tests/jersey-2/src/main/java/com/restserver/Resource.java index 7bb6ef6728d..09d90ca02eb 100644 --- a/dd-smoke-tests/jersey-2/src/main/java/com/restserver/Resource.java +++ b/dd-smoke-tests/jersey-2/src/main/java/com/restserver/Resource.java @@ -1,7 +1,7 @@ package com.restserver; +import java.net.URI; import java.net.URISyntaxException; -import java.sql.SQLException; import java.util.Map; import javax.ws.rs.Consumes; import javax.ws.rs.CookieParam; @@ -33,32 +33,28 @@ public String hello() { @Path("/bypathparam/{name}") @GET @Produces(MediaType.TEXT_PLAIN) - public String byPathParam(@PathParam("name") String name) throws SQLException { - DB.store(name); + public String byPathParam(@PathParam("name") String name) { return "Jersey: hello " + name; } @Path("/byqueryparam") @GET @Produces(MediaType.TEXT_PLAIN) - public String byQueryParam(@QueryParam("param") String param) throws SQLException { - DB.store(param); + public String byQueryParam(@QueryParam("param") String param) { return "Jersey: hello " + param; } @Path("/byheader") @GET @Produces(MediaType.TEXT_PLAIN) - public String byHeader(@HeaderParam("X-Custom-header") String param) throws SQLException { - DB.store(param); + public String byHeader(@HeaderParam("X-Custom-header") String param) { return "Jersey: hello " + param; } @Path("/bycookie") @GET @Produces(MediaType.TEXT_PLAIN) - public String byCookie(@CookieParam("cookieName") String param) throws SQLException { - DB.store(param); + public String byCookie(@CookieParam("cookieName") String param) { return "Jersey: hello " + param; } @@ -66,60 +62,73 @@ public String byCookie(@CookieParam("cookieName") String param) throws SQLExcept @POST @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - public Response put(TestEntity testEntity) throws SQLException, URISyntaxException { - DB.store(testEntity.param1); + public Response put(TestEntity testEntity) { return Response.status(Status.CREATED).build(); } @GET @Path("/cookiename") - public String sourceCookieName(@Context final HttpHeaders headers) throws SQLException { + public String sourceCookieName(@Context final HttpHeaders headers) { Map cookies = headers.getCookies(); for (Cookie cookie : cookies.values()) { - String cookieName = cookie.getName(); - DB.store(cookieName); - return "Jersey: hello " + cookieName; + if (cookie.getName().equalsIgnoreCase("cookieName")) { + String cookieName = cookie.getName(); + return "Jersey: hello " + cookieName; + } } return "cookie not found"; } @GET @Path("/headername") - public String sourceHeaderName(@Context final HttpHeaders headers) throws SQLException { + public String sourceHeaderName(@Context final HttpHeaders headers) { for (String headerName : headers.getRequestHeaders().keySet()) { - DB.store(headerName); - return "Jersey: header stored"; + if (headerName.equalsIgnoreCase("X-Custom-header")) { + return "Jersey: hello " + headerName; + } } - return "cookie not found"; + return "header not found"; } @GET @Path("/cookieobjectvalue") - public String sourceCookieValue(@Context final HttpHeaders headers) throws SQLException { + public String sourceCookieValue(@Context final HttpHeaders headers) { Map cookies = headers.getCookies(); for (Cookie cookie : cookies.values()) { - String cookieValue = cookie.getValue(); - DB.store(cookieValue); - return "Jersey: hello " + cookieValue; + if (cookie.getName().equalsIgnoreCase("cookieName")) { + String cookieValue = cookie.getValue(); + return "Jersey: hello " + cookieValue; + } } return "cookie not found"; } @POST @Path("/formparameter") - public String sourceParameterName(@FormParam("formParam1Name") final String formParam1Value) - throws SQLException { - DB.store(formParam1Value); + public String sourceParameterName(@FormParam("formParam1Name") final String formParam1Value) { return String.format("Jersey: hello " + formParam1Value); } @POST @Path("/formparametername") - public String sourceParameterName(Form form) throws SQLException { + public String sourceParameterName(Form form) { for (String paramName : form.asMap().keySet()) { - DB.store(paramName); - return "Jersey: hello " + paramName; + if (paramName.equalsIgnoreCase("formParam1Name")) { + return "Jersey: hello " + paramName; + } } - return String.format("Parameter name not found"); + return "Parameter name not found"; + } + + @Path("/setlocationheader") + @GET + public Response locationHeader(@QueryParam("param") String param) { + return Response.status(Response.Status.TEMPORARY_REDIRECT).header("Location", param).build(); + } + + @Path("/setresponselocation") + @GET + public Response responseLocation(@QueryParam("param") String param) throws URISyntaxException { + return Response.status(Response.Status.TEMPORARY_REDIRECT).location(new URI(param)).build(); } } diff --git a/dd-smoke-tests/jersey-2/src/test/groovy/datadog/smoketest/Jersey2SmokeTest.groovy b/dd-smoke-tests/jersey-2/src/test/groovy/datadog/smoketest/Jersey2SmokeTest.groovy index 8221c863250..8526f708d41 100644 --- a/dd-smoke-tests/jersey-2/src/test/groovy/datadog/smoketest/Jersey2SmokeTest.groovy +++ b/dd-smoke-tests/jersey-2/src/test/groovy/datadog/smoketest/Jersey2SmokeTest.groovy @@ -2,294 +2,28 @@ package datadog.smoketest import datadog.trace.api.Platform import datadog.trace.api.config.IastConfig -import datadog.trace.test.agent.decoder.DecodedSpan -import groovy.json.JsonSlurper -import okhttp3.FormBody -import okhttp3.MediaType -import okhttp3.Request -import okhttp3.RequestBody -import spock.util.concurrent.PollingConditions -import java.util.function.Function -import java.util.function.Predicate - -class Jersey2SmokeTest extends AbstractServerSmokeTest { - private static final String TAG_NAME = '_dd.iast.json' - - @Override - def logLevel(){ - return "debug" - } - - @Override - Closure decodedTracesCallback() { - return {} // force traces decoding - } +class Jersey2SmokeTest extends AbstractJerseySmokeTest { @Override ProcessBuilder createProcessBuilder() { - String jarPath = System.getProperty("datadog.smoketest.jersey2.jar.path") + String jarPath = System.getProperty('datadog.smoketest.jersey2.jar.path') - List command = new ArrayList<>() + List command = [] command.add(javaPath()) command.addAll(defaultJavaProperties) command.addAll([ withSystemProperty(IastConfig.IAST_ENABLED, true), - withSystemProperty(IastConfig.IAST_REQUEST_SAMPLING, 100), + withSystemProperty(IastConfig.IAST_DETECTION_MODE, 'FULL'), withSystemProperty(IastConfig.IAST_DEBUG_ENABLED, true), - withSystemProperty(IastConfig.IAST_DEDUPLICATION_ENABLED, false), - withSystemProperty(IastConfig.IAST_REDACTION_ENABLED, false), - withSystemProperty("integration.grizzly.enabled", true) + withSystemProperty('integration.grizzly.enabled', true) ]) - if (Platform.isJavaVersionAtLeast(17)){ - command.addAll((String[]) ["--add-opens", "java.base/java.lang=ALL-UNNAMED"]) + if (Platform.isJavaVersionAtLeast(17)) { + command.addAll((String[]) ['--add-opens', 'java.base/java.lang=ALL-UNNAMED']) } - command.addAll((String[]) ["-jar", jarPath, httpPort]) + command.addAll((String[]) ['-jar', jarPath, httpPort]) ProcessBuilder processBuilder = new ProcessBuilder(command) processBuilder.directory(new File(buildDirectory)) - } - - def "test put json injected bean"(){ - setup: - def url = "http://localhost:${httpPort}/hello/puttest" - def json = "{\"param1\":\"param1Value\",\"param2\":\"param2Value\"}" - def requestBody = RequestBody.create(MediaType.parse("application/json"), json) - - when: - def request = new Request.Builder().url(url).post(requestBody).build() - def response = client.newCall(request).execute() - - then: - assert response.code() == 201 - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('SQL_INJECTION').and(evidence('param1Value')))) - } - - def "Test path parameter"() { - setup: - def url = "http://localhost:${httpPort}/hello/bypathparam/pathParamValue" - - when: - def request = new Request.Builder().url(url).get().build() - def response = client.newCall(request).execute() - - then: - String body = response.body().string() - assert body != null - assert response.body().contentType().toString().contains("text/plain") - assert body.contains("Jersey: hello pathParamValue") - assert response.code() == 200 - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('SQL_INJECTION').and(evidence('pathParamValue')))) - } - - def "query parameter"() { - setup: - def url = "http://localhost:${httpPort}/hello/byqueryparam?param=queryParamValue" - - when: - def request = new Request.Builder().url(url).get().build() - def response = client.newCall(request).execute() - - then: - String body = response.body().string() - assert body != null - assert response.body().contentType().toString().contains("text/plain") - assert body.contains("Jersey: hello queryParamValue") - assert response.code() == 200 - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('SQL_INJECTION').and(evidence('queryParamValue')))) - } - - - def "header"() { - setup: - def url = "http://localhost:${httpPort}/hello/byheader" - - when: - def request = new Request.Builder().url(url).header("X-Custom-header", "pepito").get().build() - def response = client.newCall(request).execute() - - then: - String body = response.body().string() - assert body != null - assert response.body().contentType().toString().contains("text/plain") - assert body.contains("Jersey: hello pepito") - assert response.code() == 200 - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('SQL_INJECTION').and(evidence('pepito')))) - } - - def "header name"() { - setup: - def url = "http://localhost:${httpPort}/hello/headername" - - when: - def request = new Request.Builder().url(url).header("X-Custom-header", "pepito").get().build() - def response = client.newCall(request).execute() - - then: - String body = response.body().string() - assert body != null - assert response.body().contentType().toString().contains("text/plain") - assert body.contains("Jersey: header stored") - assert response.code() == 200 - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('SQL_INJECTION'))) - } - - - def "cookie"() { - setup: - def url = "http://localhost:${httpPort}/hello/bycookie" - - when: - def request = new Request.Builder().url(url).addHeader("Cookie", "cookieName=cookieValue").get().build() - def response = client.newCall(request).execute() - - then: - String body = response.body().string() - assert body != null - assert response.body().contentType().toString().contains("text/plain") - assert body.contains("Jersey: hello cookieValue") - assert response.code() == 200 - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('SQL_INJECTION').and(evidence('cookieValue')))) - } - - def "cookie name from Cookie object"() { - setup: - def url = "http://localhost:${httpPort}/hello/cookiename" - - when: - def request = new Request.Builder().url(url).addHeader("Cookie", "cookieName=cookieValue").get().build() - def response = client.newCall(request).execute() - - then: - String body = response.body().string() - assert body != null - assert response.body().contentType().toString().contains("text/plain") - assert body.contains("Jersey: hello cookieName") - assert response.code() == 200 - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('SQL_INJECTION').and(evidence('cookieName')))) - } - - def "cookie value from Cookie object"() { - setup: - def url = "http://localhost:${httpPort}/hello/cookieobjectvalue" - - when: - def request = new Request.Builder().url(url).addHeader("Cookie", "cookieName=cookieObjectValue").get().build() - def response = client.newCall(request).execute() - - then: - String body = response.body().string() - assert body != null - assert response.body().contentType().toString().contains("text/plain") - assert body.contains("Jersey: hello cookieObjectValue") - assert response.code() == 200 - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('SQL_INJECTION').and(evidence('cookieObjectValue')))) - } - - def "form parameter values"() { - setup: - def url = "http://localhost:${httpPort}/hello/formparameter" - - when: - def formBody = new FormBody.Builder() - formBody.add("formParam1Name", "formParam1Value") - def request = new Request.Builder().url(url).post(formBody.build()).build() - def response = client.newCall(request).execute() - - then: - String body = response.body().string() - assert body != null - assert response.body().contentType().toString().contains("text/plain") - assert body.contains("Jersey: hello formParam1Value") - assert response.code() == 200 - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('SQL_INJECTION').and(evidence('formParam1Value')))) - } - - def "form parameter name"() { - setup: - def url = "http://localhost:${httpPort}/hello/formparametername" - - when: - def formBody = new FormBody.Builder() - formBody.add("formParam1Name", "formParam1Value") - def request = new Request.Builder().url(url).post(formBody.build()).build() - def response = client.newCall(request).execute() - - then: - String body = response.body().string() - assert body != null - assert response.body().contentType().toString().contains("text/plain") - assert body.contains("Jersey: hello formParam1Name") - assert response.code() == 200 - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('SQL_INJECTION').and(evidence('formParam1Name')))) - } - - - - private static String withSystemProperty(final String config, final Object value) { - return "-Ddd.${config}=${value}" - } - - private static Function hasVulnerability(final Predicate predicate) { - return { span -> - final iastMeta = span.meta.get(TAG_NAME) - if (!iastMeta) { - return false - } - final vulnerabilities = parseVulnerabilities(iastMeta) - return vulnerabilities.stream().anyMatch(predicate) - } - } - - private static Predicate type(final String type) { - return new Predicate() { - @Override - boolean test(Object vul) { - return vul['type'] == type - } - } - } - - private static Collection parseVulnerabilities(final String log, final int startIndex) { - final chars = log.toCharArray() - final builder = new StringBuilder() - def level = 0 - for (int i = log.indexOf('{', startIndex); i < chars.length; i++) { - final current = chars[i] - if (current == '{' as char) { - level++ - } else if (current == '}' as char) { - level-- - } - builder.append(chars[i]) - if (level == 0) { - break - } - } - return parseVulnerabilities(builder.toString()) - } - - private static Collection parseVulnerabilities(final String iastJson) { - final slurper = new JsonSlurper() - final parsed = slurper.parseText(iastJson) - return parsed['vulnerabilities'] as Collection - } - - private static Predicate evidence(final String value) { - return new Predicate() { - @Override - boolean test(Object vul) { - return vul['evidence']['valueParts'][1]['value'] == value - } - } + return processBuilder } } diff --git a/dd-smoke-tests/jersey-3/build.gradle b/dd-smoke-tests/jersey-3/build.gradle index b0f2ce024a7..f569b3d16f5 100644 --- a/dd-smoke-tests/jersey-3/build.gradle +++ b/dd-smoke-tests/jersey-3/build.gradle @@ -1,5 +1,6 @@ plugins { id "com.github.johnrengelman.shadow" + id 'java-test-fixtures' } apply from: "$rootDir/gradle/java.gradle" @@ -16,8 +17,8 @@ dependencies { implementation group: 'org.glassfish.jersey.inject', name: 'jersey-hk2', version:'3.0.2' implementation group: 'org.glassfish.hk2', name: 'hk2-metadata-generator', version:'3.0.2' implementation group: 'jakarta.activation', name: 'jakarta.activation-api', version:'2.0.1' - implementation group: 'com.h2database', name: 'h2', version: '1.3.148' testImplementation project(':dd-smoke-tests') + testImplementation(testFixtures(project(":dd-smoke-tests:iast-util"))) } tasks.withType(Test).configureEach { diff --git a/dd-smoke-tests/jersey-3/src/main/java/smoketest/DB.java b/dd-smoke-tests/jersey-3/src/main/java/smoketest/DB.java deleted file mode 100644 index 52814dc039a..00000000000 --- a/dd-smoke-tests/jersey-3/src/main/java/smoketest/DB.java +++ /dev/null @@ -1,31 +0,0 @@ -package smoketest; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; - -public class DB { - - public static void main(String[] args) throws SQLException { - DB.store("pepe"); - } - - @SuppressFBWarnings - public static void store(String value) throws SQLException { - try (Connection conn = DriverManager.getConnection("jdbc:h2:mem:test_mem"); - Statement st = conn.createStatement()) { - st.execute("create table pepe (title VARCHAR(50) NOT NULL) "); - st.executeUpdate( - new StringBuilder("insert into pepe values('").append(value).append("')").toString()); - try (ResultSet rs = st.executeQuery("select * from pepe")) { - rs.next(); - if (!rs.getString(1).equals(value)) { - throw new SQLException("Value " + value + " not found in db"); - } - } - } - } -} diff --git a/dd-smoke-tests/jersey-3/src/main/java/smoketest/Resource.java b/dd-smoke-tests/jersey-3/src/main/java/smoketest/Resource.java index eae48a4b08d..0cd2494a1b2 100644 --- a/dd-smoke-tests/jersey-3/src/main/java/smoketest/Resource.java +++ b/dd-smoke-tests/jersey-3/src/main/java/smoketest/Resource.java @@ -17,7 +17,6 @@ import jakarta.ws.rs.core.Response; import java.net.URI; import java.net.URISyntaxException; -import java.sql.SQLException; import java.util.Map; @Path("/hello") @@ -32,85 +31,83 @@ public String hello() { @Path("/bypathparam/{name}") @GET @Produces(MediaType.TEXT_PLAIN) - public String byPathParam(@PathParam("name") String name) throws SQLException { - DB.store(name); + public String byPathParam(@PathParam("name") String name) { return "Jersey: hello " + name; } @Path("/byqueryparam") @GET @Produces(MediaType.TEXT_PLAIN) - public String byQueryParam(@QueryParam("param") String param) throws SQLException { - DB.store(param); + public String byQueryParam(@QueryParam("param") String param) { return "Jersey: hello " + param; } @Path("/byheader") @GET @Produces(MediaType.TEXT_PLAIN) - public String byHeader(@HeaderParam("X-Custom-header") String param) throws SQLException { - DB.store(param); + public String byHeader(@HeaderParam("X-Custom-header") String param) { return "Jersey: hello " + param; } @Path("/bycookie") @GET @Produces(MediaType.TEXT_PLAIN) - public String byCookie(@CookieParam("cookieName") String param) throws SQLException { - DB.store(param); + public String byCookie(@CookieParam("cookieName") String param) { return "Jersey: hello " + param; } @GET @Path("/cookiename") - public String sourceCookieName(@Context final HttpHeaders headers) throws SQLException { + public String sourceCookieName(@Context final HttpHeaders headers) { Map cookies = headers.getCookies(); for (Cookie cookie : cookies.values()) { - String cookieName = cookie.getName(); - DB.store(cookieName); - return "Jersey: hello " + cookieName; + if (cookie.getName().equalsIgnoreCase("cookieName")) { + String cookieName = cookie.getName(); + return "Jersey: hello " + cookieName; + } } return "cookie not found"; } @GET @Path("/headername") - public String sourceHeaderName(@Context final HttpHeaders headers) throws SQLException { + public String sourceHeaderName(@Context final HttpHeaders headers) { for (String headerName : headers.getRequestHeaders().keySet()) { - DB.store(headerName); - return "Jersey: hello " + headerName; + if (headerName.equalsIgnoreCase("X-Custom-header")) { + return "Jersey: hello " + headerName; + } } - return "cookie not found"; + return "header not found"; } @GET @Path("/cookieobjectvalue") - public String sourceCookieValue(@Context final HttpHeaders headers) throws SQLException { + public String sourceCookieValue(@Context final HttpHeaders headers) { Map cookies = headers.getCookies(); for (Cookie cookie : cookies.values()) { - String cookieValue = cookie.getValue(); - DB.store(cookieValue); - return "Jersey: hello " + cookieValue; + if (cookie.getName().equalsIgnoreCase("cookieName")) { + String cookieValue = cookie.getValue(); + return "Jersey: hello " + cookieValue; + } } return "cookie not found"; } @POST @Path("/formparameter") - public String sourceParameterName(@FormParam("formParam1Name") final String formParam1Value) - throws SQLException { - DB.store(formParam1Value); + public String sourceParameterName(@FormParam("formParam1Name") final String formParam1Value) { return String.format("Jersey: hello " + formParam1Value); } @POST @Path("/formparametername") - public String sourceParameterName(Form form) throws SQLException { + public String sourceParameterName(Form form) { for (String paramName : form.asMap().keySet()) { - DB.store(paramName); - return "Jersey: hello " + paramName; + if (paramName.equalsIgnoreCase("formParam1Name")) { + return "Jersey: hello " + paramName; + } } - return String.format("Parameter name not found"); + return "Parameter name not found"; } @Path("/setlocationheader") diff --git a/dd-smoke-tests/jersey-3/src/test/groovy/datadog/smoketest/Jersey3SmokeTest.groovy b/dd-smoke-tests/jersey-3/src/test/groovy/datadog/smoketest/Jersey3SmokeTest.groovy index 225d7bf030b..d20c3cce31d 100644 --- a/dd-smoke-tests/jersey-3/src/test/groovy/datadog/smoketest/Jersey3SmokeTest.groovy +++ b/dd-smoke-tests/jersey-3/src/test/groovy/datadog/smoketest/Jersey3SmokeTest.groovy @@ -1,310 +1,27 @@ package datadog.smoketest -import datadog.trace.test.agent.decoder.DecodedSpan -import groovy.json.JsonSlurper -import okhttp3.FormBody -import okhttp3.Request -import spock.util.concurrent.PollingConditions +import static datadog.trace.api.config.IastConfig.* -import java.util.function.Function -import java.util.function.Predicate - -import static datadog.trace.api.config.IastConfig.IAST_DEBUG_ENABLED -import static datadog.trace.api.config.IastConfig.IAST_DEDUPLICATION_ENABLED -import static datadog.trace.api.config.IastConfig.IAST_ENABLED -import static datadog.trace.api.config.IastConfig.IAST_REDACTION_ENABLED -import static datadog.trace.api.config.IastConfig.IAST_REQUEST_SAMPLING - -class Jersey3SmokeTest extends AbstractServerSmokeTest { - - private static final String TAG_NAME = '_dd.iast.json' - - @Override - def logLevel() { - return "debug" - } - - @Override - Closure decodedTracesCallback() { - return {} // force traces decoding - } +class Jersey3SmokeTest extends AbstractJerseySmokeTest { @Override ProcessBuilder createProcessBuilder() { - String jarPath = System.getProperty("datadog.smoketest.jersey3.jar.path") + String jarPath = System.getProperty('datadog.smoketest.jersey3.jar.path') - List command = new ArrayList<>() + List command = [] command.add(javaPath()) command.addAll(defaultJavaProperties) command.addAll([ withSystemProperty(IAST_ENABLED, true), - withSystemProperty(IAST_REQUEST_SAMPLING, 100), + withSystemProperty(IAST_DETECTION_MODE, 'FULL'), withSystemProperty(IAST_DEBUG_ENABLED, true), - withSystemProperty(IAST_DEDUPLICATION_ENABLED, false), - withSystemProperty(IAST_REDACTION_ENABLED, false), - withSystemProperty("integration.grizzly.enabled", true) + withSystemProperty('integration.grizzly.enabled', true) ]) //command.add("-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000") //command.add("-Xdebug") - command.addAll((String[]) ["-jar", jarPath, httpPort]) + command.addAll((String[]) ['-jar', jarPath, httpPort]) ProcessBuilder processBuilder = new ProcessBuilder(command) processBuilder.directory(new File(buildDirectory)) - } - - def "path parameter"() { - setup: - def url = "http://localhost:${httpPort}/hello/bypathparam/pathParamValue" - - when: - def request = new Request.Builder().url(url).get().build() - def response = client.newCall(request).execute() - - then: - String body = response.body().string() - assert body != null - assert response.body().contentType().toString().contains("text/plain") - assert body.contains("Jersey: hello pathParamValue") - assert response.code() == 200 - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('SQL_INJECTION').and(evidence('pathParamValue')))) - } - - def "query parameter"() { - setup: - def url = "http://localhost:${httpPort}/hello/byqueryparam?param=queryParamValue" - - when: - def request = new Request.Builder().url(url).get().build() - def response = client.newCall(request).execute() - - then: - String body = response.body().string() - assert body != null - assert response.body().contentType().toString().contains("text/plain") - assert body.contains("Jersey: hello queryParamValue") - assert response.code() == 200 - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('SQL_INJECTION').and(evidence('queryParamValue')))) - } - - - def "header"() { - setup: - def url = "http://localhost:${httpPort}/hello/byheader" - - when: - def request = new Request.Builder().url(url).header("X-Custom-header", "pepito").get().build() - def response = client.newCall(request).execute() - - then: - String body = response.body().string() - assert body != null - assert response.body().contentType().toString().contains("text/plain") - assert body.contains("Jersey: hello pepito") - assert response.code() == 200 - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('SQL_INJECTION').and(evidence('pepito')))) - } - - def "header name"() { - setup: - def url = "http://localhost:${httpPort}/hello/headername" - - when: - def request = new Request.Builder().url(url).header("X-Custom-header", "pepito").get().build() - def response = client.newCall(request).execute() - - then: - String body = response.body().string() - assert body != null - assert response.body().contentType().toString().contains("text/plain") - assert body.contains("Jersey: hello x-custom-header") - assert response.code() == 200 - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('SQL_INJECTION').and(evidence('x-custom-header')))) - } - - - def "cookie"() { - setup: - def url = "http://localhost:${httpPort}/hello/bycookie" - - when: - def request = new Request.Builder().url(url).addHeader("Cookie", "cookieName=cookieValue").get().build() - def response = client.newCall(request).execute() - - then: - String body = response.body().string() - assert body != null - assert response.body().contentType().toString().contains("text/plain") - assert body.contains("Jersey: hello cookieValue") - assert response.code() == 200 - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('SQL_INJECTION').and(evidence('cookieValue')))) - } - - - void "unvalidated redirect from location header is present"() { - setup: - def url = "http://localhost:${httpPort}/hello/setlocationheader?param=queryParamValue" - - when: - def request = new Request.Builder().url(url).get().build() - def response = client.newCall(request).execute() - - then: - response.isRedirect() - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('UNVALIDATED_REDIRECT'))) - } - - void "unvalidated redirect from location is present"() { - setup: - def url = "http://localhost:${httpPort}/hello/setresponselocation?param=queryParamValue" - - when: - def request = new Request.Builder().url(url).get().build() - def response = client.newCall(request).execute() - - then: - response.isRedirect() - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('UNVALIDATED_REDIRECT'))) - } - - def "cookie name from Cookie object"() { - setup: - def url = "http://localhost:${httpPort}/hello/cookiename" - - when: - def request = new Request.Builder().url(url).addHeader("Cookie", "cookieName=cookieValue").get().build() - def response = client.newCall(request).execute() - - then: - String body = response.body().string() - assert body != null - assert response.body().contentType().toString().contains("text/plain") - assert body.contains("Jersey: hello cookieName") - assert response.code() == 200 - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('SQL_INJECTION').and(evidence('cookieName')))) - } - - def "cookie value from Cookie object"() { - setup: - def url = "http://localhost:${httpPort}/hello/cookieobjectvalue" - - when: - def request = new Request.Builder().url(url).addHeader("Cookie", "cookieName=cookieObjectValue").get().build() - def response = client.newCall(request).execute() - - then: - String body = response.body().string() - assert body != null - assert response.body().contentType().toString().contains("text/plain") - assert body.contains("Jersey: hello cookieObjectValue") - assert response.code() == 200 - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('SQL_INJECTION').and(evidence('cookieObjectValue')))) - } - - def "form parameter values"() { - setup: - def url = "http://localhost:${httpPort}/hello/formparameter" - - when: - def formBody = new FormBody.Builder() - formBody.add("formParam1Name", "formParam1Value") - def request = new Request.Builder().url(url).post(formBody.build()).build() - def response = client.newCall(request).execute() - - then: - String body = response.body().string() - assert body != null - assert response.body().contentType().toString().contains("text/plain") - assert body.contains("Jersey: hello formParam1Value") - assert response.code() == 200 - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('SQL_INJECTION').and(evidence('formParam1Value')))) - } - - def "form parameter name"() { - setup: - def url = "http://localhost:${httpPort}/hello/formparametername" - - when: - def formBody = new FormBody.Builder() - formBody.add("formParam1Name", "formParam1Value") - def request = new Request.Builder().url(url).post(formBody.build()).build() - def response = client.newCall(request).execute() - - then: - String body = response.body().string() - assert body != null - assert response.body().contentType().toString().contains("text/plain") - assert body.contains("Jersey: hello formParam1Name") - assert response.code() == 200 - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('SQL_INJECTION').and(evidence('formParam1Name')))) - } - - - - private static String withSystemProperty(final String config, final Object value) { - return "-Ddd.${config}=${value}" - } - - private static Function hasVulnerability(final Predicate predicate) { - return { span -> - final iastMeta = span.meta.get(TAG_NAME) - if (!iastMeta) { - return false - } - final vulnerabilities = parseVulnerabilities(iastMeta) - return vulnerabilities.stream().anyMatch(predicate) - } - } - - private static Predicate type(final String type) { - return new Predicate() { - @Override - boolean test(Object vul) { - return vul['type'] == type - } - } - } - - private static Collection parseVulnerabilities(final String log, final int startIndex) { - final chars = log.toCharArray() - final builder = new StringBuilder() - def level = 0 - for (int i = log.indexOf('{', startIndex); i < chars.length; i++) { - final current = chars[i] - if (current == '{' as char) { - level++ - } else if (current == '}' as char) { - level-- - } - builder.append(chars[i]) - if (level == 0) { - break - } - } - return parseVulnerabilities(builder.toString()) - } - - private static Collection parseVulnerabilities(final String iastJson) { - final slurper = new JsonSlurper() - final parsed = slurper.parseText(iastJson) - return parsed['vulnerabilities'] as Collection - } - - private static Predicate evidence(final String value) { - return new Predicate() { - @Override - boolean test(Object vul) { - return vul['evidence']['valueParts'][1]['value'] == value - } - } + return processBuilder } } diff --git a/dd-smoke-tests/resteasy/build.gradle b/dd-smoke-tests/resteasy/build.gradle index 598923773a7..e3ca38289c2 100644 --- a/dd-smoke-tests/resteasy/build.gradle +++ b/dd-smoke-tests/resteasy/build.gradle @@ -1,5 +1,6 @@ plugins { id "com.github.johnrengelman.shadow" + id 'java-test-fixtures' } apply from: "$rootDir/gradle/java.gradle" @@ -17,12 +18,12 @@ dependencies { implementation group: 'org.jboss.weld.servlet', name: 'weld-servlet', version: '2.4.8.Final' implementation group: 'javax.el', name: 'javax.el-api', version:'3.0.0' - implementation group: 'com.h2database', name: 'h2', version: '1.3.148' implementation "jakarta.xml.bind:jakarta.xml.bind-api:2.3.2" implementation "org.glassfish.jaxb:jaxb-runtime:2.3.2" testImplementation project(':dd-smoke-tests') + testImplementation(testFixtures(project(":dd-smoke-tests:iast-util"))) } tasks.withType(Test).configureEach { diff --git a/dd-smoke-tests/resteasy/src/main/java/smoketest/resteasy/DB.java b/dd-smoke-tests/resteasy/src/main/java/smoketest/resteasy/DB.java deleted file mode 100644 index 5cf0af89dfa..00000000000 --- a/dd-smoke-tests/resteasy/src/main/java/smoketest/resteasy/DB.java +++ /dev/null @@ -1,32 +0,0 @@ -package smoketest.resteasy; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; - -public class DB { - - public static void main(String[] args) throws SQLException { - DB.store("pepe"); - } - - @SuppressFBWarnings - public static void store(String value) throws SQLException { - try (Connection conn = DriverManager.getConnection("jdbc:h2:mem:test_mem"); - Statement st = conn.createStatement()) { - st.execute("create table pepe (title VARCHAR(50) NOT NULL) "); - st.executeUpdate( - new StringBuilder("insert into pepe values('").append(value).append("')").toString()); - System.out.println("Inserted value " + value); - try (ResultSet rs = st.executeQuery("select * from pepe")) { - rs.next(); - if (!rs.getString(1).equals(value)) { - throw new SQLException("Value " + value + " not found in db"); - } - } - } - } -} 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 d8e8d10e36d..734f774a7ea 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 @@ -2,7 +2,6 @@ import java.net.URI; import java.net.URISyntaxException; -import java.sql.SQLException; import java.util.List; import java.util.Set; import java.util.SortedSet; @@ -30,58 +29,49 @@ public String hello() { @Path("/bypathparam/{name}") @GET @Produces(MediaType.TEXT_PLAIN) - public String byPathParam(@PathParam("name") String name) throws SQLException { - DB.store(name); + public String byPathParam(@PathParam("name") String name) { return "RestEasy: hello " + name; } @Path("/byqueryparam") @GET @Produces(MediaType.TEXT_PLAIN) - public String byQueryParam(@QueryParam("param") String param) throws SQLException { - DB.store(param); + public String byQueryParam(@QueryParam("param") String param) { return "RestEasy: hello " + param; } @Path("/byheader") @GET @Produces(MediaType.TEXT_PLAIN) - public String byHeader(@HeaderParam("X-Custom-header") String param) throws SQLException { - DB.store(param); + public String byHeader(@HeaderParam("X-Custom-header") String param) { return "RestEasy: hello " + param; } @Path("/bycookie") @GET @Produces(MediaType.TEXT_PLAIN) - public String byCookie(@CookieParam("cookieName") String param) throws SQLException { - DB.store(param); + public String byCookie(@CookieParam("cookieName") String param) { return "RestEasy: hello " + param; } @Path("/collection") @GET @Produces(MediaType.TEXT_PLAIN) - public String collectionByQueryParam(@QueryParam("param") List param) - throws SQLException { - DB.store(param.get(0)); + public String collectionByQueryParam(@QueryParam("param") List param) { return "RestEasy: hello " + param; } @Path("/set") @GET @Produces(MediaType.TEXT_PLAIN) - public String setByQueryParam(@QueryParam("param") Set param) throws SQLException { - DB.store(param.iterator().next()); + public String setByQueryParam(@QueryParam("param") Set param) { return "RestEasy: hello " + param; } @Path("/sortedset") @GET @Produces(MediaType.TEXT_PLAIN) - public String sortedSetByQueryParam(@QueryParam("param") SortedSet param) - throws SQLException { - DB.store(param.iterator().next()); + public String sortedSetByQueryParam(@QueryParam("param") SortedSet param) { return "RestEasy: hello " + param; } diff --git a/dd-smoke-tests/resteasy/src/test/groovy/smoketest/ResteasySmokeTest.groovy b/dd-smoke-tests/resteasy/src/test/groovy/smoketest/ResteasySmokeTest.groovy index 657f8b87b1c..2e87d51f4fb 100644 --- a/dd-smoke-tests/resteasy/src/test/groovy/smoketest/ResteasySmokeTest.groovy +++ b/dd-smoke-tests/resteasy/src/test/groovy/smoketest/ResteasySmokeTest.groovy @@ -1,6 +1,6 @@ package smoketest -import datadog.smoketest.AbstractServerSmokeTest +import datadog.smoketest.AbstractIastServerSmokeTest import datadog.trace.api.Platform import okhttp3.Request import spock.lang.IgnoreIf @@ -8,13 +8,7 @@ import spock.lang.IgnoreIf @IgnoreIf({ System.getProperty("java.vendor").contains("IBM") && System.getProperty("java.version").contains("1.8.") }) -class ResteasySmokeTest extends AbstractServerSmokeTest { - - - @Override - def logLevel() { - return "debug" - } +class ResteasySmokeTest extends AbstractIastServerSmokeTest { @Override ProcessBuilder createProcessBuilder() { @@ -25,10 +19,8 @@ class ResteasySmokeTest extends AbstractServerSmokeTest { command.addAll(defaultJavaProperties) command.addAll([ withSystemProperty(datadog.trace.api.config.IastConfig.IAST_ENABLED, true), - withSystemProperty(datadog.trace.api.config.IastConfig.IAST_REQUEST_SAMPLING, 100), - withSystemProperty(datadog.trace.api.config.IastConfig.IAST_DEBUG_ENABLED, true), - withSystemProperty(datadog.trace.api.config.IastConfig.IAST_DEDUPLICATION_ENABLED, false), - withSystemProperty(datadog.trace.api.config.IastConfig.IAST_REDACTION_ENABLED, false) + withSystemProperty(datadog.trace.api.config.IastConfig.IAST_DETECTION_MODE, 'FULL'), + withSystemProperty(datadog.trace.api.config.IastConfig.IAST_DEBUG_ENABLED, true) ]) if (Platform.isJavaVersionAtLeast(17)) { command.addAll((String[]) ["--add-opens", "java.base/java.lang=ALL-UNNAMED"]) @@ -52,8 +44,9 @@ class ResteasySmokeTest extends AbstractServerSmokeTest { assert response.body().contentType().toString().contains("text/plain") assert body.contains("RestEasy: hello pathParamValue") assert response.code() == 200 - processTestLogLines { - it.contains("SQL_INJECTION") && it.contains("smoketest.resteasy.DB") && it.contains("pathParamValue") + hasTainted { tainted -> + tainted.value == 'pathParamValue' && + tainted.ranges[0].source.origin == 'http.request.path.parameter' } } @@ -71,8 +64,10 @@ class ResteasySmokeTest extends AbstractServerSmokeTest { assert response.body().contentType().toString().contains("text/plain") assert body.contains("RestEasy: hello queryParamValue") assert response.code() == 200 - processTestLogLines { - it.contains("SQL_INJECTION") && it.contains("smoketest.resteasy.DB") && it.contains("queryParamValue") + hasTainted { tainted -> + tainted.value == 'queryParamValue' && + tainted.ranges[0].source.name == 'param' && + tainted.ranges[0].source.origin == 'http.request.parameter' } } @@ -90,8 +85,10 @@ class ResteasySmokeTest extends AbstractServerSmokeTest { assert response.body().contentType().toString().contains("text/plain") assert body.contains("RestEasy: hello pepito") assert response.code() == 200 - processTestLogLines { - it.contains("SQL_INJECTION") && it.contains("smoketest.resteasy.DB") && it.contains("pepito") + hasTainted { tainted -> + tainted.value == 'pepito' && + tainted.ranges[0].source.name == 'X-Custom-header' && + tainted.ranges[0].source.origin == 'http.request.header' } } @@ -109,8 +106,10 @@ class ResteasySmokeTest extends AbstractServerSmokeTest { assert response.body().contentType().toString().contains("text/plain") assert body.contains("RestEasy: hello cookieValue") assert response.code() == 200 - processTestLogLines { - it.contains("SQL_INJECTION") && it.contains("smoketest.resteasy.DB") && it.contains("cookieValue") + hasTainted { tainted -> + tainted.value == 'cookieValue' && + tainted.ranges[0].source.name == 'cookieName' && + tainted.ranges[0].source.origin == 'http.request.cookie.value' } } @@ -128,8 +127,15 @@ class ResteasySmokeTest extends AbstractServerSmokeTest { assert response.body().contentType().toString().contains("text/plain") assert body.contains("RestEasy: hello [value1, value2]") assert response.code() == 200 - processTestLogLines { - it.contains("SQL_INJECTION") && it.contains("smoketest.resteasy.DB") && it.contains("value1") + hasTainted { tainted -> + tainted.value == 'value1' && + tainted.ranges[0].source.name == 'param' && + tainted.ranges[0].source.origin == 'http.request.parameter' + } + hasTainted { tainted -> + tainted.value == 'value2' && + tainted.ranges[0].source.name == 'param' && + tainted.ranges[0].source.origin == 'http.request.parameter' } } @@ -147,8 +153,15 @@ class ResteasySmokeTest extends AbstractServerSmokeTest { assert response.body().contentType().toString().contains("text/plain") assert body.contains("setValue1") assert response.code() == 200 - processTestLogLines { - it.contains("SQL_INJECTION") && it.contains("smoketest.resteasy.DB") && it.contains("setValue1") + hasTainted { tainted -> + tainted.value == 'setValue1' && + tainted.ranges[0].source.name == 'param' && + tainted.ranges[0].source.origin == 'http.request.parameter' + } + hasTainted { tainted -> + tainted.value == 'setValue2' && + tainted.ranges[0].source.name == 'param' && + tainted.ranges[0].source.origin == 'http.request.parameter' } } @@ -166,8 +179,15 @@ class ResteasySmokeTest extends AbstractServerSmokeTest { assert response.body().contentType().toString().contains("text/plain") assert body.contains("sortedsetValue1") assert response.code() == 200 - processTestLogLines { - it.contains("SQL_INJECTION") && it.contains("smoketest.resteasy.DB") && it.contains("sortedsetValue1") + hasTainted { tainted -> + tainted.value == 'sortedsetValue1' && + tainted.ranges[0].source.name == 'param' && + tainted.ranges[0].source.origin == 'http.request.parameter' + } + hasTainted { tainted -> + tainted.value == 'sortedsetValue2' && + tainted.ranges[0].source.name == 'param' && + tainted.ranges[0].source.origin == 'http.request.parameter' } } @@ -180,9 +200,7 @@ class ResteasySmokeTest extends AbstractServerSmokeTest { client.newCall(request).execute() then: - processTestLogLines { - it.contains("UNVALIDATED_REDIRECT") && it.contains("setheader") - } + hasVulnerability { vul -> vul.type == 'UNVALIDATED_REDIRECT' } } void "unvalidated redirect from location header is present"() { @@ -194,13 +212,6 @@ class ResteasySmokeTest extends AbstractServerSmokeTest { client.newCall(request).execute() then: - processTestLogLines { - it.contains("UNVALIDATED_REDIRECT") && it.contains("setlocation") - } - } - - - private static String withSystemProperty(final String config, final Object value) { - return "-Ddd.${config}=${value}" + hasVulnerability { vul -> vul.type == 'UNVALIDATED_REDIRECT' } } } diff --git a/dd-smoke-tests/spring-boot-2.6-webmvc/build.gradle b/dd-smoke-tests/spring-boot-2.6-webmvc/build.gradle index 0ea4214673c..abb4140aa8f 100644 --- a/dd-smoke-tests/spring-boot-2.6-webmvc/build.gradle +++ b/dd-smoke-tests/spring-boot-2.6-webmvc/build.gradle @@ -1,5 +1,6 @@ plugins { id "com.github.johnrengelman.shadow" + id 'java-test-fixtures' } @@ -38,6 +39,7 @@ dependencies { testImplementation project(':dd-smoke-tests') implementation project(':dd-smoke-tests:iast-util') + testImplementation(testFixtures(project(":dd-smoke-tests:iast-util"))) } tasks.withType(Test).configureEach { diff --git a/dd-smoke-tests/spring-boot-2.6-webmvc/src/main/java/datadog/smoketest/springboot/SpringbootApplication.java b/dd-smoke-tests/spring-boot-2.6-webmvc/src/main/java/datadog/smoketest/springboot/SpringbootApplication.java index e26a5350e0e..a4d6e6b42e9 100644 --- a/dd-smoke-tests/spring-boot-2.6-webmvc/src/main/java/datadog/smoketest/springboot/SpringbootApplication.java +++ b/dd-smoke-tests/spring-boot-2.6-webmvc/src/main/java/datadog/smoketest/springboot/SpringbootApplication.java @@ -1,13 +1,46 @@ package datadog.smoketest.springboot; +import datadog.smoketest.springboot.controller.SimpleIastController; +import java.util.Collections; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; +import org.springframework.web.servlet.mvc.Controller; +import org.springframework.web.util.UrlPathHelper; @SpringBootApplication @EnableJpaRepositories public class SpringbootApplication { + @Configuration + public static class WebConfig extends WebMvcConfigurerAdapter { + + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + UrlPathHelper urlPathHelper = new UrlPathHelper(); + urlPathHelper.setRemoveSemicolonContent(false); + configurer.setUrlPathHelper(urlPathHelper); + } + + @Bean + public Controller simpleIastController() { + return new SimpleIastController(); + } + + @Bean + public SimpleUrlHandlerMapping simpleMapping(Controller simpleIastController) { + SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); + mapping.setUrlMap(Collections.singletonMap("/simple/{var1}", simpleIastController)); + mapping.setOrder(0); + return mapping; + } + } + public static void main(final String[] args) { SpringApplication.run(SpringbootApplication.class, args); } diff --git a/dd-smoke-tests/spring-boot-2.6-webmvc/src/main/java/datadog/smoketest/springboot/controller/IastWebController.java b/dd-smoke-tests/spring-boot-2.6-webmvc/src/main/java/datadog/smoketest/springboot/controller/IastWebController.java index 02ba546629c..f103c6bfea3 100644 --- a/dd-smoke-tests/spring-boot-2.6-webmvc/src/main/java/datadog/smoketest/springboot/controller/IastWebController.java +++ b/dd-smoke-tests/spring-boot-2.6-webmvc/src/main/java/datadog/smoketest/springboot/controller/IastWebController.java @@ -4,14 +4,21 @@ import ddtest.client.sources.Hasher; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.io.File; +import java.io.IOException; +import java.net.HttpCookie; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import javax.websocket.server.PathParam; +import org.springframework.http.HttpStatus; +import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.MatrixVariable; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; @@ -40,6 +47,47 @@ public String weakhash() { return "Weak Hash page"; } + @GetMapping("/insecure_cookie") + public String insecureCookie(HttpServletResponse response) { + Cookie cookie = new Cookie("user-id", "7"); + response.addCookie(cookie); + response.setStatus(HttpStatus.OK.value()); + return "Insecure cookie page"; + } + + @GetMapping("/secure_cookie") + public String secureCookie(HttpServletResponse response) { + Cookie cookie = new Cookie("user-id", "7"); + cookie.setSecure(true); + response.addCookie(cookie); + response.setStatus(HttpStatus.OK.value()); + return "Insecure cookie page"; + } + + @GetMapping("/insecure_cookie_from_header") + public String insecureCookieFromHeader(HttpServletResponse response) { + HttpCookie cookie = new HttpCookie("user-id", "7"); + + response.addHeader("Set-Cookie", cookie.toString()); + response.setStatus(HttpStatus.OK.value()); + return "Insecure cookie page"; + } + + @GetMapping("/unvalidated_redirect_from_header") + public String unvalidatedRedirectFromHeader( + @RequestParam String param, HttpServletResponse response) { + response.addHeader("Location", param); + response.setStatus(HttpStatus.FOUND.value()); + return "Unvalidated redirect"; + } + + @GetMapping("/unvalidated_redirect_from_send_redirect") + public String unvalidatedRedirectFromSendRedirect( + @RequestParam String param, HttpServletResponse response) throws IOException { + response.sendRedirect(param); + return "Unvalidated redirect"; + } + @RequestMapping("/async_weakhash") public String asyncWeakhash() { final Thread thread = new Thread(hasher::md4); @@ -99,6 +147,14 @@ public String pathParam(@PathParam("param") String param) { return "PathParam is: " + param; } + @GetMapping("/matrix/{var1}/{var2}") + public String matrixAndPathVariables( + @PathVariable String var1, + @MatrixVariable(pathVar = "var1") MultiValueMap m1, + @MatrixVariable(pathVar = "var2") MultiValueMap m2) { + return "{var1=" + var1 + ", m1=" + m1 + ", m2=" + m2 + "}"; + } + @PostMapping("/request_body/test") public String jsonRequestBody(@RequestBody TestBean testBean) { return "@RequestBody to Test bean -> name: " diff --git a/dd-smoke-tests/spring-boot-2.6-webmvc/src/main/java/datadog/smoketest/springboot/controller/SimpleIastController.java b/dd-smoke-tests/spring-boot-2.6-webmvc/src/main/java/datadog/smoketest/springboot/controller/SimpleIastController.java new file mode 100644 index 00000000000..fee5a93c926 --- /dev/null +++ b/dd-smoke-tests/spring-boot-2.6-webmvc/src/main/java/datadog/smoketest/springboot/controller/SimpleIastController.java @@ -0,0 +1,24 @@ +package datadog.smoketest.springboot.controller; + +import java.io.PrintWriter; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.AbstractController; + +public class SimpleIastController extends AbstractController { + @Override + protected ModelAndView handleRequestInternal( + HttpServletRequest request, HttpServletResponse response) throws Exception { + Map vars = + (Map) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + PrintWriter printWriter = response.getWriter(); + response.setHeader("Content-type", "text/plain"); + printWriter.write("Template variables:"); + printWriter.write(vars.toString()); + printWriter.write('\n'); + return null; + } +} diff --git a/dd-smoke-tests/spring-boot-2.6-webmvc/src/test/groovy/IastSpringBootSmokeTest.groovy b/dd-smoke-tests/spring-boot-2.6-webmvc/src/test/groovy/IastSpringBootSmokeTest.groovy index e74b31ec3c3..4f9e9c2da18 100644 --- a/dd-smoke-tests/spring-boot-2.6-webmvc/src/test/groovy/IastSpringBootSmokeTest.groovy +++ b/dd-smoke-tests/spring-boot-2.6-webmvc/src/test/groovy/IastSpringBootSmokeTest.groovy @@ -1,459 +1,6 @@ -import datadog.smoketest.AbstractServerSmokeTest -import datadog.trace.test.agent.decoder.DecodedSpan -import groovy.json.JsonSlurper +import datadog.smoketest.AbstractIastSpringBootTest import groovy.transform.CompileDynamic -import okhttp3.FormBody -import okhttp3.MediaType -import okhttp3.Request -import okhttp3.RequestBody -import spock.util.concurrent.PollingConditions - -import java.util.concurrent.TimeoutException -import java.util.function.Function -import java.util.function.Predicate - -import static datadog.trace.api.config.IastConfig.* @CompileDynamic -class IastSpringBootSmokeTest extends AbstractServerSmokeTest { - - private static final String TAG_NAME = '_dd.iast.json' - - private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8") - - @Override - def logLevel() { - return "debug" - } - - @Override - Closure decodedTracesCallback() { - return {} // force traces decoding - } - - @Override - ProcessBuilder createProcessBuilder() { - String springBootShadowJar = System.getProperty("datadog.smoketest.springboot.shadowJar.path") - - List command = new ArrayList<>() - command.add(javaPath()) - command.addAll(defaultJavaProperties) - command.addAll([ - withSystemProperty(IAST_ENABLED, true), - withSystemProperty(IAST_REQUEST_SAMPLING, 100), - withSystemProperty(IAST_DEBUG_ENABLED, true), - withSystemProperty(IAST_REDACTION_ENABLED, false) - ]) - command.addAll((String[]) ["-jar", springBootShadowJar, "--server.port=${httpPort}"]) - ProcessBuilder processBuilder = new ProcessBuilder(command) - processBuilder.directory(new File(buildDirectory)) - // Spring will print all environment variables to the log, which may pollute it and affect log assertions. - processBuilder.environment().clear() - processBuilder - } - - void "IAST subsystem starts"() { - given: 'an initial request has succeeded' - String url = "http://localhost:${httpPort}/greeting" - def request = new Request.Builder().url(url).get().build() - client.newCall(request).execute() - - when: 'logs are read' - String startMsg = null - String errorMsg = null - checkLogPostExit { - if (it.contains("Not starting IAST subsystem")) { - errorMsg = it - } - if (it.contains("IAST is starting")) { - startMsg = it - } - // Check that there's no logged exception about missing classes from Datadog. - // We had this problem before with JDK9StackWalker. - if (it.contains("java.lang.ClassNotFoundException: datadog/")) { - errorMsg = it - } - } - - then: 'there are no errors in the log and IAST has started' - errorMsg == null - startMsg != null - !logHasErrors - } - - void "default home page without errors"() { - setup: - String url = "http://localhost:${httpPort}/greeting" - def request = new Request.Builder().url(url).get().build() - - when: - def response = client.newCall(request).execute() - - then: - def responseBodyStr = response.body().string() - responseBodyStr != null - responseBodyStr.contains("Sup Dawg") - response.body().contentType().toString().contains("text/plain") - response.code() == 200 - - checkLogPostExit() - !logHasErrors - } - - void "iast.enabled tag is present"() { - setup: - String url = "http://localhost:${httpPort}/greeting" - def request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - waitForSpan(new PollingConditions(timeout: 5), hasMetric('_dd.iast.enabled', 1)) - } - - void "weak hash vulnerability is present"() { - setup: - String url = "http://localhost:${httpPort}/weakhash" - def request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('WEAK_HASH').and(evidence('MD5')))) - } - - void "weak hash vulnerability is present on boot"() { - setup: - String url = "http://localhost:${httpPort}/greeting" - def request = new Request.Builder().url(url).get().build() - - when: 'ensure the controller is loaded' - client.newCall(request).execute() - - then: 'a vulnerability pops in the logs (startup traces might not always be available)' - hasVulnerabilityInLogs(type('WEAK_HASH').and(evidence('SHA1')).and(withSpan())) - } - - void "weak hash vulnerability is present on thread"() { - setup: - String url = "http://localhost:${httpPort}/async_weakhash" - def request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('WEAK_HASH').and(evidence('MD4')).and(withSpan()))) - } - - void "getParameter taints string"() { - setup: - String url = "http://localhost:${httpPort}/getparameter?param=A" - def request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - hasTainted { tainted -> - tainted.value == 'A' && - tainted.ranges[0].source.name == 'param' && - tainted.ranges[0].source.origin == 'http.request.parameter' - } - } - - void "command injection is present with runtime"() { - setup: - final url = "http://localhost:${httpPort}/cmdi/runtime?cmd=ls" - final request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - waitForSpan(new PollingConditions(timeout: 5), hasVulnerability(type("COMMAND_INJECTION"))) - } - - void "command injection is present with process builder"() { - setup: - final url = "http://localhost:${httpPort}/cmdi/process_builder?cmd=ls" - final request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - waitForSpan(new PollingConditions(timeout: 5), hasVulnerability(type("COMMAND_INJECTION"))) - } - - void "path traversal is present with file"() { - setup: - final url = "http://localhost:${httpPort}/path_traversal/file?path=test" - final request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - waitForSpan(new PollingConditions(timeout: 5), hasVulnerability(type("PATH_TRAVERSAL"))) - } - - void "path traversal is present with paths"() { - setup: - final url = "http://localhost:${httpPort}/path_traversal/paths?path=test" - final request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - waitForSpan(new PollingConditions(timeout: 5), hasVulnerability(type("PATH_TRAVERSAL"))) - } - - void "path traversal is present with path"() { - setup: - final url = "http://localhost:${httpPort}/path_traversal/path?path=test" - final request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - waitForSpan(new PollingConditions(timeout: 5), hasVulnerability(type("PATH_TRAVERSAL"))) - } - - void "parameter binding taints bean strings"() { - setup: - String url = "http://localhost:${httpPort}/param_binding/test?name=parameter&value=binding" - def request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - hasTainted { tainted -> - tainted.value == 'binding' && - tainted.ranges[0].source.name == 'value' && - tainted.ranges[0].source.origin == 'http.request.parameter' - } - } - - void "request header taint string"() { - setup: - String url = "http://localhost:${httpPort}/request_header/test" - def request = new Request.Builder().url(url).header("test-header", "test").get().build() - - when: - client.newCall(request).execute() - - then: - hasTainted { tainted -> - tainted.value == 'test' && - tainted.ranges[0].source.name == 'test-header' && - tainted.ranges[0].source.origin == 'http.request.header' - } - } - - void "path param taint string"() { - setup: - String url = "http://localhost:${httpPort}/path_param?param=test" - def request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - hasTainted { tainted -> - tainted.value == 'test' && - tainted.ranges[0].source.name == 'param' && - tainted.ranges[0].source.origin == 'http.request.parameter' - } - } - - void "request body taint json"() { - setup: - String url = "http://localhost:${httpPort}/request_body/test" - def request = new Request.Builder().url(url).post(RequestBody.create(JSON, '{"name": "nameTest", "value" : "valueTest"}')).build() - - when: - client.newCall(request).execute() - - then: - hasTainted { tainted -> - tainted.value == 'nameTest' && - tainted.ranges[0].source.origin == 'http.request.body' - } - } - - void 'request query string'() { - given: - final url = "http://localhost:${httpPort}/query_string?key=value" - final request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - hasTainted { tainted -> - tainted.value == 'key=value' && - tainted.ranges[0].source.origin == 'http.request.query' - } - } - - void 'request cookie propagation'() { - given: - final url = "http://localhost:${httpPort}/cookie" - final request = new Request.Builder().url(url).header('Cookie', 'name=value').get().build() - - when: - client.newCall(request).execute() - - then: - hasTainted { tainted -> - tainted.value == 'name' && - tainted.ranges[0].source.origin == 'http.request.cookie.name' - } - hasTainted { tainted -> - tainted.value == 'value' && - tainted.ranges[0].source.name == 'name' && - tainted.ranges[0].source.origin == 'http.request.cookie.value' - } - } - - void 'ssrf is present'() { - setup: - final url = "http://localhost:${httpPort}/ssrf" - final body = new FormBody.Builder().add('url', 'https://dd.datad0g.com/').build() - final request = new Request.Builder().url(url).post(body).build() - - when: - client.newCall(request).execute() - - then: - waitForSpan(new PollingConditions(timeout: 5), hasVulnerability(type('SSRF'))) - } - - void 'test iast metrics stored in spans'() { - setup: - final url = "http://localhost:${httpPort}/cmdi/runtime?cmd=ls" - final request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - waitForSpan(new PollingConditions(timeout: 5), - hasMetric('_dd.iast.telemetry.executed.sink.command_injection', 1)) - } - - private static Function hasMetric(final String name, final Object value) { - return new Function() { - @Override - Boolean apply(DecodedSpan decodedSpan) { - return value == decodedSpan.metrics.get(name) - } - } - } - - private static Function hasVulnerability(final Predicate predicate) { - return { span -> - final iastMeta = span.meta.get(TAG_NAME) - if (!iastMeta) { - return false - } - final vulnerabilities = parseVulnerabilities(iastMeta) - return vulnerabilities.stream().anyMatch(predicate) - } - } - - private boolean hasVulnerabilityInLogs(final Predicate predicate) { - def found = false - checkLogPostExit { final String log -> - final index = log.indexOf(TAG_NAME) - if (index >= 0) { - final vulnerabilities = parseVulnerabilities(log, index) - found |= vulnerabilities.stream().anyMatch(predicate) - } - } - return found - } - - private void hasTainted(final Closure matcher) { - final slurper = new JsonSlurper() - final tainteds = [] - try { - processTestLogLines { String log -> - final index = log.indexOf('tainted=') - if (index >= 0) { - final tainted = slurper.parse(new StringReader(log.substring(index + 8))) - tainteds.add(tainted) - if (matcher.call(tainted)) { - return true // found - } - } - } - } catch (TimeoutException toe) { - throw new AssertionError("No matching tainted found. Tainteds found: ${tainteds}") - } - } - - private static Collection parseVulnerabilities(final String log, final int startIndex) { - final chars = log.toCharArray() - final builder = new StringBuilder() - def level = 0 - for (int i = log.indexOf('{', startIndex); i < chars.length; i++) { - final current = chars[i] - if (current == '{' as char) { - level++ - } else if (current == '}' as char) { - level-- - } - builder.append(chars[i]) - if (level == 0) { - break - } - } - return parseVulnerabilities(builder.toString()) - } - - private static Collection parseVulnerabilities(final String iastJson) { - final slurper = new JsonSlurper() - final parsed = slurper.parseText(iastJson) - return parsed['vulnerabilities'] as Collection - } - - private static Predicate type(final String type) { - return new Predicate() { - @Override - boolean test(Object vul) { - return vul['type'] == type - } - } - } - - private static Predicate evidence(final String value) { - return new Predicate() { - @Override - boolean test(Object vul) { - return vul['evidence']['value'] == value - } - } - } - - private static Predicate withSpan() { - return new Predicate() { - @Override - boolean test(Object vul) { - return vul['location']['spanId'] > 0 - } - } - } - - private static String withSystemProperty(final String config, final Object value) { - return "-Ddd.${config}=${value}" - } +class IastSpringBootSmokeTest extends AbstractIastSpringBootTest { } diff --git a/dd-smoke-tests/spring-security/build.gradle b/dd-smoke-tests/spring-security/build.gradle index 211a2c8604c..acba7da69db 100644 --- a/dd-smoke-tests/spring-security/build.gradle +++ b/dd-smoke-tests/spring-security/build.gradle @@ -1,3 +1,6 @@ +plugins { + id 'java-test-fixtures' +} ext { maxJavaVersionForTests = JavaVersion.VERSION_15 @@ -24,6 +27,7 @@ dependencies { testImplementation project(':dd-smoke-tests') implementation project(':dd-smoke-tests:iast-util') + testImplementation(testFixtures(project(":dd-smoke-tests:iast-util"))) } diff --git a/dd-smoke-tests/spring-security/src/test/groovy/datadog/smoketest/SpringSecurityJwtTest.groovy b/dd-smoke-tests/spring-security/src/test/groovy/datadog/smoketest/SpringSecurityJwtTest.groovy index 54db9bcd5c0..1c34697b990 100644 --- a/dd-smoke-tests/spring-security/src/test/groovy/datadog/smoketest/SpringSecurityJwtTest.groovy +++ b/dd-smoke-tests/spring-security/src/test/groovy/datadog/smoketest/SpringSecurityJwtTest.groovy @@ -9,30 +9,23 @@ import com.nimbusds.jose.jwk.RSAKey import com.nimbusds.jose.jwk.gen.RSAKeyGenerator import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT -import groovy.json.JsonSlurper import groovy.transform.CompileDynamic import okhttp3.Request import okhttp3.Response import java.security.KeyPair import java.text.ParseException -import java.util.concurrent.TimeoutException import java.util.stream.Collectors import static datadog.trace.api.config.IastConfig.IAST_DEBUG_ENABLED +import static datadog.trace.api.config.IastConfig.IAST_DETECTION_MODE import static datadog.trace.api.config.IastConfig.IAST_ENABLED -import static datadog.trace.api.config.IastConfig.IAST_REQUEST_SAMPLING @CompileDynamic -class SpringSecurityJwtTest extends AbstractServerSmokeTest { +class SpringSecurityJwtTest extends AbstractIastServerSmokeTest { static final RSAKey rsaJWK = new RSAKeyGenerator(2048).generate() - @Override - def logLevel() { - return "debug" - } - @Override ProcessBuilder createProcessBuilder() { KeyPair kp = rsaJWK.toKeyPair() @@ -46,7 +39,7 @@ class SpringSecurityJwtTest extends AbstractServerSmokeTest { command.addAll(defaultJavaProperties) command.addAll([ withSystemProperty(IAST_ENABLED, true), - withSystemProperty(IAST_REQUEST_SAMPLING, 100), + withSystemProperty(IAST_DETECTION_MODE, 'FULL'), withSystemProperty(IAST_DEBUG_ENABLED, true), ]) command.addAll((String[]) [ @@ -94,10 +87,6 @@ class SpringSecurityJwtTest extends AbstractServerSmokeTest { } } - private static String withSystemProperty(final String config, final Object value) { - return "-Ddd.${config}=${value}" - } - private static String buildClassPath(){ String cp = System.getProperty("java.class.path") String separator = System.getProperty("path.separator") @@ -120,25 +109,6 @@ class SpringSecurityJwtTest extends AbstractServerSmokeTest { return mainJar + ":" + result } - private void hasTainted(final Closure matcher) { - final slurper = new JsonSlurper() - final tainteds = [] - try { - processTestLogLines { String log -> - final index = log.indexOf('tainted=') - if (index >= 0) { - final tainted = slurper.parse(new StringReader(log.substring(index + 8))) - tainteds.add(tainted) - if (matcher.call(tainted)) { - return true - } - } - } - } catch (TimeoutException toe) { - throw new AssertionError("No matching tainted found. Tainteds found: ${tainteds}") - } - } - String generateToken(RSAKey rsaJWK) throws JOSEException, ParseException { JWSSigner signer = new RSASSASigner(rsaJWK) diff --git a/dd-smoke-tests/springboot/build.gradle b/dd-smoke-tests/springboot/build.gradle index a931d356f34..de5cc97237c 100644 --- a/dd-smoke-tests/springboot/build.gradle +++ b/dd-smoke-tests/springboot/build.gradle @@ -1,5 +1,6 @@ plugins { id "com.github.johnrengelman.shadow" + id 'java-test-fixtures' } ext { @@ -27,6 +28,8 @@ dependencies { implementation group: 'org.springframework', name: 'spring-web', version: '1.5.18.RELEASE' testImplementation project(':dd-smoke-tests') + testImplementation(testFixtures(project(":dd-smoke-tests:iast-util"))) + implementation project(':dd-smoke-tests:iast-util') } diff --git a/dd-smoke-tests/springboot/src/main/java/datadog/smoketest/springboot/controller/IastWebController.java b/dd-smoke-tests/springboot/src/main/java/datadog/smoketest/springboot/controller/IastWebController.java index ddf8c8e3a72..6cc30c2005d 100644 --- a/dd-smoke-tests/springboot/src/main/java/datadog/smoketest/springboot/controller/IastWebController.java +++ b/dd-smoke-tests/springboot/src/main/java/datadog/smoketest/springboot/controller/IastWebController.java @@ -98,12 +98,6 @@ public String asyncWeakhash() { @RequestMapping("/getparameter") public String getParameter(@RequestParam String param, HttpServletRequest request) { - // StringWriter sw = new StringWriter(); - // PrintWriter pw = new PrintWriter(sw); - // new Throwable().printStackTrace(pw); - // return sw.toString(); - // TestSuite testSuite = new TestSuite(new HttpServletRequestWrapper(request)); - // testSuite.getParameterMap(); return "Param is: " + param; } @@ -186,6 +180,7 @@ public String jwt(Principal userPrincipal) { return "ok User Principal name: " + userPrincipal.getName(); } + @SuppressWarnings("CatchMayIgnoreException") @PostMapping("/ssrf") public String ssrf(@RequestParam("url") final String url) { try { diff --git a/dd-smoke-tests/springboot/src/test/groovy/datadog/smoketest/AbstractSpringBootIastTest.groovy b/dd-smoke-tests/springboot/src/test/groovy/datadog/smoketest/AbstractSpringBootIastTest.groovy deleted file mode 100644 index 7bc9e41a23e..00000000000 --- a/dd-smoke-tests/springboot/src/test/groovy/datadog/smoketest/AbstractSpringBootIastTest.groovy +++ /dev/null @@ -1,110 +0,0 @@ -package datadog.smoketest - -import datadog.trace.test.agent.decoder.DecodedSpan -import groovy.json.JsonSlurper -import okhttp3.MediaType - -import java.util.concurrent.TimeoutException -import java.util.function.Function -import java.util.function.Predicate - -abstract class AbstractSpringBootIastTest extends AbstractServerSmokeTest { - - protected static final String TAG_NAME = '_dd.iast.json' - - protected static final MediaType JSON = MediaType.parse("application/json; charset=utf-8") - - protected static Function hasMetric(final String name, final Object value) { - return { span -> value == span.metrics.get(name) } - } - - protected static Function hasVulnerability(final Predicate predicate) { - return { span -> - final iastMeta = span.meta.get(TAG_NAME) - if (!iastMeta) { - return false - } - final vulnerabilities = parseVulnerabilities(iastMeta) - return vulnerabilities.stream().anyMatch(predicate) - } - } - - protected boolean hasVulnerabilityInLogs(final Predicate predicate) { - def found = false - checkLogPostExit { final String log -> - final index = log.indexOf(TAG_NAME) - if (index >= 0) { - final vulnerabilities = parseVulnerabilities(log, index) - found |= vulnerabilities.stream().anyMatch(predicate) - } - } - return found - } - - protected void hasTainted(final Closure matcher) { - final slurper = new JsonSlurper() - final tainteds = [] - try { - processTestLogLines { String log -> - final index = log.indexOf('tainted=') - if (index >= 0) { - final tainted = slurper.parse(new StringReader(log.substring(index + 8))) - tainteds.add(tainted) - if (matcher.call(tainted)) { - return true // found - } - } - } - } catch (TimeoutException toe) { - throw new AssertionError("No matching tainted found. Tainteds found: ${tainteds}") - } - } - - - protected static Collection parseVulnerabilities(final String log, final int startIndex) { - final chars = log.toCharArray() - final builder = new StringBuilder() - def level = 0 - for (int i = log.indexOf('{', startIndex); i < chars.length; i++) { - final current = chars[i] - if (current == '{' as char) { - level++ - } else if (current == '}' as char) { - level-- - } - builder.append(chars[i]) - if (level == 0) { - break - } - } - return parseVulnerabilities(builder.toString()) - } - - protected static Collection parseVulnerabilities(final String iastJson) { - final slurper = new JsonSlurper() - final parsed = slurper.parseText(iastJson) - return parsed['vulnerabilities'] as Collection - } - - protected static Predicate type(final String type) { - return { vul -> - vul.type == type - } - } - - protected static Predicate evidence(final String value) { - return { vul -> - vul.evidence.value == value - } - } - - protected static Predicate withSpan() { - return { vul -> - vul.location.spanId > 0 - } - } - - protected static String withSystemProperty(final String config, final Object value) { - return "-Ddd.${config}=${value}" - } -} diff --git a/dd-smoke-tests/springboot/src/test/groovy/datadog/smoketest/IastSpringBootRedirectSmokeTest.groovy b/dd-smoke-tests/springboot/src/test/groovy/datadog/smoketest/IastSpringBootRedirectSmokeTest.groovy index 0d3c9752b79..ffa9dec0db8 100644 --- a/dd-smoke-tests/springboot/src/test/groovy/datadog/smoketest/IastSpringBootRedirectSmokeTest.groovy +++ b/dd-smoke-tests/springboot/src/test/groovy/datadog/smoketest/IastSpringBootRedirectSmokeTest.groovy @@ -2,25 +2,14 @@ package datadog.smoketest import groovy.transform.CompileDynamic import okhttp3.Request -import spock.util.concurrent.PollingConditions import static datadog.trace.api.config.IastConfig.IAST_DEBUG_ENABLED +import static datadog.trace.api.config.IastConfig.IAST_DETECTION_MODE import static datadog.trace.api.config.IastConfig.IAST_ENABLED import static datadog.trace.api.config.IastConfig.IAST_REDACTION_ENABLED -import static datadog.trace.api.config.IastConfig.IAST_REQUEST_SAMPLING @CompileDynamic -class IastSpringBootRedirectSmokeTest extends AbstractSpringBootIastTest { - - @Override - def logLevel() { - return "debug" - } - - @Override - Closure decodedTracesCallback() { - return {} // force traces decoding - } +class IastSpringBootRedirectSmokeTest extends AbstractIastServerSmokeTest { @Override ProcessBuilder createProcessBuilder() { @@ -31,7 +20,7 @@ class IastSpringBootRedirectSmokeTest extends AbstractSpringBootIastTest { command.addAll(defaultJavaProperties) command.addAll([ withSystemProperty(IAST_ENABLED, true), - withSystemProperty(IAST_REQUEST_SAMPLING, 100), + withSystemProperty(IAST_DETECTION_MODE, 'FULL'), withSystemProperty(IAST_DEBUG_ENABLED, true), withSystemProperty(IAST_REDACTION_ENABLED, false) ]) @@ -54,8 +43,7 @@ class IastSpringBootRedirectSmokeTest extends AbstractSpringBootIastTest { then: response.isRedirect() response.header("Location").contains("redirected") - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('UNVALIDATED_REDIRECT'))) + hasVulnerability { vul -> vul.type == 'UNVALIDATED_REDIRECT' } } def "unvalidated redirect from sendRedirect is present"() { @@ -68,6 +56,9 @@ class IastSpringBootRedirectSmokeTest extends AbstractSpringBootIastTest { then: response.isRedirect() - hasVulnerabilityInLogs(type('UNVALIDATED_REDIRECT').and(withSpan())) + // TODO: span deserialization fails when checking the vulnerability + // === Failure during message v0.4 decoding === + //org.msgpack.core.MessageTypeException: Expected String, but got Nil (c0) + hasVulnerabilityInLogs { vul -> vul.type == 'UNVALIDATED_REDIRECT' } } } diff --git a/dd-smoke-tests/springboot/src/test/groovy/datadog/smoketest/IastSpringBootSmokeTest.groovy b/dd-smoke-tests/springboot/src/test/groovy/datadog/smoketest/IastSpringBootSmokeTest.groovy index 00fe896c609..628ed6901e3 100644 --- a/dd-smoke-tests/springboot/src/test/groovy/datadog/smoketest/IastSpringBootSmokeTest.groovy +++ b/dd-smoke-tests/springboot/src/test/groovy/datadog/smoketest/IastSpringBootSmokeTest.groovy @@ -1,451 +1,28 @@ package datadog.smoketest import groovy.transform.CompileDynamic -import okhttp3.FormBody import okhttp3.Request -import okhttp3.RequestBody import okhttp3.Response -import spock.util.concurrent.PollingConditions - -import static datadog.trace.api.config.IastConfig.IAST_DEBUG_ENABLED -import static datadog.trace.api.config.IastConfig.IAST_ENABLED -import static datadog.trace.api.config.IastConfig.IAST_REDACTION_ENABLED -import static datadog.trace.api.config.IastConfig.IAST_REQUEST_SAMPLING @CompileDynamic -class IastSpringBootSmokeTest extends AbstractSpringBootIastTest { - - @Override - def logLevel() { - return "debug" - } - - @Override - Closure decodedTracesCallback() { - return {} // force traces decoding - } - - @Override - ProcessBuilder createProcessBuilder() { - String springBootShadowJar = System.getProperty("datadog.smoketest.springboot.shadowJar.path") - - List command = new ArrayList<>() - command.add(javaPath()) - command.addAll(defaultJavaProperties) - command.addAll([ - withSystemProperty(IAST_ENABLED, true), - withSystemProperty(IAST_REQUEST_SAMPLING, 100), - withSystemProperty(IAST_DEBUG_ENABLED, true), - withSystemProperty(IAST_REDACTION_ENABLED, false) - ]) - command.addAll((String[]) ["-jar", springBootShadowJar, "--server.port=${httpPort}"]) - ProcessBuilder processBuilder = new ProcessBuilder(command) - processBuilder.directory(new File(buildDirectory)) - // Spring will print all environment variables to the log, which may pollute it and affect log assertions. - processBuilder.environment().clear() - processBuilder - } - - def "IAST subsystem starts"() { - given: 'an initial request has succeeded' - String url = "http://localhost:${httpPort}/greeting" - def request = new Request.Builder().url(url).get().build() - client.newCall(request).execute() - - when: 'logs are read' - String startMsg = null - String errorMsg = null - checkLogPostExit { - if (it.contains("Not starting IAST subsystem")) { - errorMsg = it - } - if (it.contains("IAST is starting")) { - startMsg = it - } - // Check that there's no logged exception about missing classes from Datadog. - // We had this problem before with JDK9StackWalker. - if (it.contains("java.lang.ClassNotFoundException: datadog/")) { - errorMsg = it - } - } - - then: 'there are no errors in the log and IAST has started' - errorMsg == null - startMsg != null - !logHasErrors - } - - def "default home page without errors"() { - setup: - String url = "http://localhost:${httpPort}/greeting" - def request = new Request.Builder().url(url).get().build() - - when: - def response = client.newCall(request).execute() - - then: - def responseBodyStr = response.body().string() - responseBodyStr != null - responseBodyStr.contains("Sup Dawg") - response.body().contentType().toString().contains("text/plain") - response.code() == 200 - - checkLogPostExit() - !logHasErrors - } - - def "iast.enabled tag is present"() { - setup: - String url = "http://localhost:${httpPort}/greeting" - def request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - waitForSpan(new PollingConditions(timeout: 5), hasMetric('_dd.iast.enabled', 1)) - } - - def "weak hash vulnerability is present"() { - setup: - String url = "http://localhost:${httpPort}/weakhash" - def request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('WEAK_HASH').and(evidence('MD5')))) - } - - def "insecure cookie vulnerability is present"() { - setup: - String url = "http://localhost:${httpPort}/insecure_cookie" - def request = new Request.Builder().url(url).get().build() - - when: - def response = client.newCall(request).execute() - - then: - response.isSuccessful() - response.header("Set-Cookie").contains("user-id") - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('INSECURE_COOKIE').and(evidence('user-id')))) - } - - def "insecure cookie vulnerability from addheader is present"() { - setup: - String url = "http://localhost:${httpPort}/insecure_cookie_from_header" - def request = new Request.Builder().url(url).get().build() - - when: - def response = client.newCall(request).execute() - - then: - response.isSuccessful() - response.header("Set-Cookie").contains("user-id") - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('INSECURE_COOKIE').and(evidence('user-id')))) - } - - - def "weak hash vulnerability is present on boot"() { - setup: - String url = "http://localhost:${httpPort}/greeting" - def request = new Request.Builder().url(url).get().build() - - when: 'ensure the controller is loaded' - client.newCall(request).execute() - - then: 'a vulnerability pops in the logs (startup traces might not always be available)' - hasVulnerabilityInLogs(type('WEAK_HASH').and(evidence('SHA1')).and(withSpan())) - } - - def "weak hash vulnerability is present on thread"() { - setup: - String url = "http://localhost:${httpPort}/async_weakhash" - def request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - waitForSpan(new PollingConditions(timeout: 5), - hasVulnerability(type('WEAK_HASH').and(evidence('MD4')).and(withSpan()))) - } - - void "getParameter taints string"() { - setup: - String url = "http://localhost:${httpPort}/getparameter?param=A" - def request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - hasTainted { tainted -> - tainted.value == 'A' && - tainted.ranges[0].source.name == 'param' && - tainted.ranges[0].source.origin == 'http.request.parameter' - } - } +class IastSpringBootSmokeTest extends AbstractIastSpringBootTest { void 'tainting of jwt'() { given: String url = "http://localhost:${httpPort}/jwt" - String token = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqYWNraWUiLCJpc3MiOiJtdm5zZWFyY2gifQ.C_q7_FwlzmvzC6L3CqOnUzb6PFs9REZ3RON6_aJTxWw" - def request = new Request.Builder().url(url).header("Authorization", token).get().build() + String token = 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqYWNraWUiLCJpc3MiOiJtdm5zZWFyY2gifQ.C_q7_FwlzmvzC6L3CqOnUzb6PFs9REZ3RON6_aJTxWw' + def request = new Request.Builder().url(url).header('Authorization', token).get().build() when: Response response = client.newCall(request).execute() then: response.successful - response.body().string().contains("jackie") + response.body().string().contains('jackie') hasTainted { it.value == 'jackie' && it.ranges[0].source.origin == 'http.request.header' } } - - def "command injection is present with runtime"() { - setup: - final url = "http://localhost:${httpPort}/cmdi/runtime?cmd=ls" - final request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - waitForSpan(new PollingConditions(timeout: 5), hasVulnerability(type("COMMAND_INJECTION"))) - } - - def "command injection is present with process builder"() { - setup: - final url = "http://localhost:${httpPort}/cmdi/process_builder?cmd=ls" - final request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - waitForSpan(new PollingConditions(timeout: 5), hasVulnerability(type("COMMAND_INJECTION"))) - } - - def "path traversal is present with file"() { - setup: - final url = "http://localhost:${httpPort}/path_traversal/file?path=test" - final request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - waitForSpan(new PollingConditions(timeout: 5), hasVulnerability(type("PATH_TRAVERSAL"))) - } - - def "path traversal is present with paths"() { - setup: - final url = "http://localhost:${httpPort}/path_traversal/paths?path=test" - final request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - waitForSpan(new PollingConditions(timeout: 5), hasVulnerability(type("PATH_TRAVERSAL"))) - } - - def "path traversal is present with path"() { - setup: - final url = "http://localhost:${httpPort}/path_traversal/path?path=test" - final request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - waitForSpan(new PollingConditions(timeout: 5), hasVulnerability(type("PATH_TRAVERSAL"))) - } - - def "parameter binding taints bean strings"() { - setup: - String url = "http://localhost:${httpPort}/param_binding/test?name=parameter&value=binding" - def request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - hasTainted { tainted -> - tainted.value == 'binding' && - tainted.ranges[0].source.name == 'value' && - tainted.ranges[0].source.origin == 'http.request.parameter' - } - } - - def "request header taint string"() { - setup: - String url = "http://localhost:${httpPort}/request_header/test" - def request = new Request.Builder().url(url).header("test-header", "test").get().build() - - when: - client.newCall(request).execute() - - then: - hasTainted { tainted -> - tainted.value == 'test' && - tainted.ranges[0].source.name == 'test-header' && - tainted.ranges[0].source.origin == 'http.request.header' - } - } - - def "path param taint string"() { - setup: - String url = "http://localhost:${httpPort}/path_param?param=test" - def request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - hasTainted { tainted -> - tainted.value == 'test' && - tainted.ranges[0].source.name == 'param' && - tainted.ranges[0].source.origin == 'http.request.parameter' - } - } - - def "request body taint json"() { - setup: - String url = "http://localhost:${httpPort}/request_body/test" - def request = new Request.Builder().url(url).post(RequestBody.create(JSON, '{"name": "nameTest", "value" : "valueTest"}')).build() - - when: - client.newCall(request).execute() - - then: - hasTainted { tainted -> - tainted.value == 'nameTest' && - tainted.ranges[0].source.origin == 'http.request.body' - } - } - - void 'request query string'() { - given: - final url = "http://localhost:${httpPort}/query_string?key=value" - final request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - hasTainted { tainted -> - tainted.value == 'key=value' && - tainted.ranges[0].source.origin == 'http.request.query' - } - } - - void 'request cookie propagation'() { - given: - final url = "http://localhost:${httpPort}/cookie" - final request = new Request.Builder().url(url).header('Cookie', 'name=value').get().build() - - when: - client.newCall(request).execute() - - then: - hasTainted { tainted -> - tainted.value == 'name' && - tainted.ranges[0].source.origin == 'http.request.cookie.name' - } - hasTainted { tainted -> - tainted.value == 'value' && - tainted.ranges[0].source.name == 'name' && - tainted.ranges[0].source.origin == 'http.request.cookie.value' - } - } - - void 'tainting of path variables — simple variant'() { - given: - String url = "http://localhost:${httpPort}/simple/foobar" - def request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - hasTainted { - it.value == 'foobar' && - it.ranges[0].source.origin == 'http.request.path.parameter' && - it.ranges[0].source.name == 'var1' - } - } - - void 'tainting of path variables — RequestMappingInfoHandlerMapping variant'() { - given: - String url = "http://localhost:${httpPort}/matrix/value;xxx=aaa,bbb;yyy=ccc/zzz=ddd" - def request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - hasTainted { tainted -> - Map firstRange = tainted.ranges[0] - tainted.value == 'value' && - firstRange?.source?.origin == 'http.request.path.parameter' && - firstRange?.source?.name == 'var1' - } - ['xxx', 'aaa', 'bbb', 'yyy', 'ccc'].each { - hasTainted { tainted -> - Map firstRange = tainted.ranges[0] - firstRange?.source?.origin == 'http.request.matrix.parameter' && - firstRange?.source?.name == 'var1' - } - } - hasTainted { tainted -> - Map firstRange = tainted.ranges[0] - tainted.value == 'zzz=ddd' && - firstRange?.source?.origin == 'http.request.path.parameter' && - firstRange?.source?.name == 'var2' - } - ['zzz', 'ddd'].each { - hasTainted { tainted -> - Map firstRange = tainted.ranges[0] - tainted.value = it && - firstRange?.source?.origin == 'http.request.matrix.parameter' && - firstRange?.source?.name == 'var2' - } - } - } - - void 'ssrf is present'() { - setup: - final url = "http://localhost:${httpPort}/ssrf" - final body = new FormBody.Builder().add('url', 'https://dd.datad0g.com/').build() - final request = new Request.Builder().url(url).post(body).build() - - when: - client.newCall(request).execute() - - then: - waitForSpan(new PollingConditions(timeout: 5), hasVulnerability(type('SSRF'))) - } - - void 'test iast metrics stored in spans'() { - setup: - final url = "http://localhost:${httpPort}/cmdi/runtime?cmd=ls" - final request = new Request.Builder().url(url).get().build() - - when: - client.newCall(request).execute() - - then: - waitForSpan(new PollingConditions(timeout: 5), - hasMetric('_dd.iast.telemetry.executed.sink.command_injection', 1)) - } - } diff --git a/dd-smoke-tests/vertx-3.4/src/test/groovy/datadog/smoketest/IastVertxSmokeTest.groovy b/dd-smoke-tests/vertx-3.4/src/test/groovy/datadog/smoketest/IastVertxSmokeTest.groovy index c0d1168e113..d2106be8058 100644 --- a/dd-smoke-tests/vertx-3.4/src/test/groovy/datadog/smoketest/IastVertxSmokeTest.groovy +++ b/dd-smoke-tests/vertx-3.4/src/test/groovy/datadog/smoketest/IastVertxSmokeTest.groovy @@ -20,7 +20,7 @@ class IastVertxSmokeTest extends AbstractIastVertxSmokeTest { command.addAll((String[]) [ //'-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005', withSystemProperty(IAST_ENABLED, true), - withSystemProperty(IAST_REQUEST_SAMPLING, 100), + withSystemProperty(IAST_DETECTION_MODE, 'FULL'), withSystemProperty(IAST_DEBUG_ENABLED, true), '-Ddd.app.customlogmanager=true', "-Dvertx.http.port=${httpPort}", diff --git a/dd-smoke-tests/vertx-3.9/src/test/groovy/datadog/smoketest/IastVertxSmokeTest.groovy b/dd-smoke-tests/vertx-3.9/src/test/groovy/datadog/smoketest/IastVertxSmokeTest.groovy index adcb0585b4b..f4398f81292 100644 --- a/dd-smoke-tests/vertx-3.9/src/test/groovy/datadog/smoketest/IastVertxSmokeTest.groovy +++ b/dd-smoke-tests/vertx-3.9/src/test/groovy/datadog/smoketest/IastVertxSmokeTest.groovy @@ -17,7 +17,7 @@ class IastVertxSmokeTest extends AbstractIastVertxSmokeTest { command.addAll((String[]) [ //'-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005', withSystemProperty(IAST_ENABLED, true), - withSystemProperty(IAST_REQUEST_SAMPLING, 100), + withSystemProperty(IAST_DETECTION_MODE, 'FULL'), withSystemProperty(IAST_DEBUG_ENABLED, true), '-Ddd.app.customlogmanager=true', "-Dvertx.http.port=${httpPort}",