diff --git a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/HttpRouteHandler.java b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/HttpRouteHandler.java new file mode 100644 index 00000000000..2e6017254d8 --- /dev/null +++ b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/HttpRouteHandler.java @@ -0,0 +1,16 @@ +package com.datadog.iast; + +import datadog.trace.api.gateway.RequestContext; +import datadog.trace.api.gateway.RequestContextSlot; +import java.util.function.BiConsumer; + +public class HttpRouteHandler implements BiConsumer { + + @Override + public void accept(final RequestContext ctx, final String route) { + final IastRequestContext iastCtx = ctx.getData(RequestContextSlot.IAST); + if (iastCtx != null) { + iastCtx.setRoute(route); + } + } +} diff --git a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/IastRequestContext.java b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/IastRequestContext.java index c2960eb82a7..ebfd78c7d3e 100644 --- a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/IastRequestContext.java +++ b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/IastRequestContext.java @@ -36,6 +36,7 @@ public class IastRequestContext implements IastContext, HasMetricCollector { @Nullable private volatile String xForwardedProto; @Nullable private volatile String contentType; @Nullable private volatile String authorization; + @Nullable private volatile String route; /** * Use {@link IastRequestContext#IastRequestContext(TaintedObjects)} instead as we require more @@ -121,6 +122,15 @@ public void setAuthorization(final String authorization) { this.authorization = authorization; } + @Nullable + public String getRoute() { + return route; + } + + public void setRoute(final String route) { + this.route = route; + } + public OverheadContext getOverheadContext() { return overheadContext; } diff --git a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/IastSystem.java b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/IastSystem.java index ec63532127b..26bd081cbc2 100644 --- a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/IastSystem.java +++ b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/IastSystem.java @@ -122,6 +122,7 @@ public static void start( registerRequestStartedCallback(ss, addTelemetry, dependencies); registerRequestEndedCallback(ss, addTelemetry, dependencies); registerHeadersCallback(ss); + registerHttpRouteCallback(ss); registerGrpcServerRequestMessageCallback(ss); maybeApplySecurityControls(instrumentation); LOGGER.debug("IAST started"); @@ -246,6 +247,10 @@ private static void registerHeadersCallback(final SubscriptionService ss) { ss.registerCallback(event, handler); } + private static void registerHttpRouteCallback(final SubscriptionService ss) { + ss.registerCallback(Events.get().httpRoute(), new HttpRouteHandler()); + } + private static void registerGrpcServerRequestMessageCallback(final SubscriptionService ss) { ss.registerCallback(Events.get().grpcServerRequestMessage(), new GrpcRequestMessageHandler()); } diff --git a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/overhead/OverheadController.java b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/overhead/OverheadController.java index 0a9e9fa56ea..344ae25226f 100644 --- a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/overhead/OverheadController.java +++ b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/overhead/OverheadController.java @@ -233,7 +233,7 @@ public boolean consumeQuota( Object methodTag = rootSpan.getTag(Tags.HTTP_METHOD); method = (methodTag == null) ? "" : methodTag.toString(); Object routeTag = rootSpan.getTag(Tags.HTTP_ROUTE); - path = (routeTag == null) ? "" : routeTag.toString(); + path = (routeTag == null) ? getHttpRouteFromRequestContext(span) : routeTag.toString(); } if (!maybeSkipVulnerability(ctx, type, method, path)) { return operation.consumeQuota(ctx); @@ -316,6 +316,19 @@ public OverheadContext getContext(@Nullable final AgentSpan span) { return globalContext; } + @Nullable + public String getHttpRouteFromRequestContext(@Nullable final AgentSpan span) { + String httpRoute = null; + final RequestContext requestContext = span != null ? span.getRequestContext() : null; + if (requestContext != null) { + IastRequestContext iastRequestContext = requestContext.getData(RequestContextSlot.IAST); + if (iastRequestContext != null) { + httpRoute = iastRequestContext.getRoute(); + } + } + return httpRoute == null ? "" : httpRoute; + } + static int computeSamplingParameter(final float pct) { if (pct >= 100) { return 100; diff --git a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/HttpRouteHandlerTest.groovy b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/HttpRouteHandlerTest.groovy new file mode 100644 index 00000000000..71b5002cead --- /dev/null +++ b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/HttpRouteHandlerTest.groovy @@ -0,0 +1,39 @@ +package com.datadog.iast + +import datadog.trace.api.gateway.RequestContext +import datadog.trace.api.gateway.RequestContextSlot +import datadog.trace.test.util.DDSpecification +import groovy.transform.CompileDynamic + +@CompileDynamic +class HttpRouteHandlerTest extends DDSpecification { + void 'route is set'() { + given: + final handler = new HttpRouteHandler() + final iastCtx = Mock(IastRequestContext) + final ctx = Mock(RequestContext) + ctx.getData(RequestContextSlot.IAST) >> iastCtx + + when: + handler.accept(ctx, '/foo') + + then: + 1 * ctx.getData(RequestContextSlot.IAST) >> iastCtx + 1 * iastCtx.setRoute('/foo') + 0 * _ + } + + void 'does nothing when context missing'() { + given: + final handler = new HttpRouteHandler() + final ctx = Mock(RequestContext) + ctx.getData(RequestContextSlot.IAST) >> null + + when: + handler.accept(ctx, '/foo') + + then: + 1 * ctx.getData(RequestContextSlot.IAST) >> null + 0 * _ + } +} diff --git a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/IastSystemTest.groovy b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/IastSystemTest.groovy index 34fed3fadb8..a09eaef3eda 100644 --- a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/IastSystemTest.groovy +++ b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/IastSystemTest.groovy @@ -60,6 +60,7 @@ class IastSystemTest extends DDSpecification { 1 * ss.registerCallback(Events.get().requestStarted(), _) 1 * ss.registerCallback(Events.get().requestEnded(), _) 1 * ss.registerCallback(Events.get().requestHeader(), _) + 1 * ss.registerCallback(Events.get().httpRoute(), _) 1 * ss.registerCallback(Events.get().grpcServerRequestMessage(), _) 0 * _ TestLogCollector.drainCapturedLogs().any { it.message.contains('IAST is starting') } diff --git a/dd-java-agent/instrumentation/play-2.3/src/main/java/datadog/trace/instrumentation/play23/PlayHttpServerDecorator.java b/dd-java-agent/instrumentation/play-2.3/src/main/java/datadog/trace/instrumentation/play23/PlayHttpServerDecorator.java index a53c5a739eb..9144fb8fe1e 100644 --- a/dd-java-agent/instrumentation/play-2.3/src/main/java/datadog/trace/instrumentation/play23/PlayHttpServerDecorator.java +++ b/dd-java-agent/instrumentation/play-2.3/src/main/java/datadog/trace/instrumentation/play23/PlayHttpServerDecorator.java @@ -112,13 +112,21 @@ private void dispatchRoute(final AgentSpan span, final String route) { if (ctx == null) { return; } + // Send event to AppSec provider final CallbackProvider cbp = tracer().getCallbackProvider(RequestContextSlot.APPSEC); - if (cbp == null) { - return; + if (cbp != null) { + final BiConsumer cb = cbp.getCallback(EVENTS.httpRoute()); + if (cb != null) { + cb.accept(ctx, route); + } } - final BiConsumer cb = cbp.getCallback(EVENTS.httpRoute()); - if (cb != null) { - cb.accept(ctx, route); + // Send event to IAST provider + final CallbackProvider cbpIast = tracer().getCallbackProvider(RequestContextSlot.IAST); + if (cbpIast != null) { + final BiConsumer cb = cbpIast.getCallback(EVENTS.httpRoute()); + if (cb != null) { + cb.accept(ctx, route); + } } } catch (final Throwable t) { LOG.debug("Failed to dispatch route", t); diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play24/PlayAdvice.java b/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play24/PlayAdvice.java index 380eb2b9f91..79f4ad4fcd2 100644 --- a/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play24/PlayAdvice.java +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play24/PlayAdvice.java @@ -46,6 +46,10 @@ public static ContextScope onEnter(@Advice.Argument(value = 0, readOnly = false) req = RequestHelper.withTag(req, "_dd_HasPlayRequestSpan", "true"); + // Moved from OnMethodExit + // Call onRequest on return after tags are populated. + DECORATE.onRequest(span, req, req, (AgentSpanContext.Extracted) null); + return scope; } @@ -63,9 +67,6 @@ public static void stopTraceOnResponse( final AgentSpan playControllerSpan = spanFromContext(playControllerScope.context()); - // Call onRequest on return after tags are populated. - DECORATE.onRequest(playControllerSpan, req, req, (AgentSpanContext.Extracted) null); - if (throwable == null) { responseFuture.onComplete( new RequestCompleteCallback(playControllerSpan), diff --git a/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play24/PlayHttpServerDecorator.java b/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play24/PlayHttpServerDecorator.java index efd55a5bb23..4aa8a27cf98 100644 --- a/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play24/PlayHttpServerDecorator.java +++ b/dd-java-agent/instrumentation/play-2.4/src/main/java/datadog/trace/instrumentation/play24/PlayHttpServerDecorator.java @@ -112,13 +112,21 @@ private void dispatchRoute(final AgentSpan span, final String route) { if (ctx == null) { return; } + // Send event to AppSec provider final CallbackProvider cbp = tracer().getCallbackProvider(RequestContextSlot.APPSEC); - if (cbp == null) { - return; + if (cbp != null) { + final BiConsumer cb = cbp.getCallback(EVENTS.httpRoute()); + if (cb != null) { + cb.accept(ctx, route); + } } - final BiConsumer cb = cbp.getCallback(EVENTS.httpRoute()); - if (cb != null) { - cb.accept(ctx, route); + // Send event to IAST provider + final CallbackProvider cbpIast = tracer().getCallbackProvider(RequestContextSlot.IAST); + if (cbpIast != null) { + final BiConsumer cb = cbpIast.getCallback(EVENTS.httpRoute()); + if (cb != null) { + cb.accept(ctx, route); + } } } catch (final Throwable t) { LOG.debug("Failed to dispatch route", t); diff --git a/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/PlayAdvice.java b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/PlayAdvice.java index 62fe3b1dece..e967ccc97e9 100644 --- a/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/PlayAdvice.java +++ b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/PlayAdvice.java @@ -47,6 +47,10 @@ public static ContextScope onEnter( req = req.addAttr(HasPlayRequestSpan.KEY, HasPlayRequestSpan.INSTANCE); + // Moved from OnMethodExit + // Call onRequest on return after tags are populated. + DECORATE.onRequest(span, req, req, extractedContext); + return scope; } @@ -65,9 +69,6 @@ public static void stopTraceOnResponse( final AgentSpan playControllerSpan = spanFromContext(playControllerScope.context()); - // Call onRequest on return after tags are populated. - DECORATE.onRequest(playControllerSpan, req, req, extractedContext); - if (throwable == null) { responseFuture.onComplete( new RequestCompleteCallback(playControllerSpan), diff --git a/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/PlayHttpServerDecorator.java b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/PlayHttpServerDecorator.java index 494546754cf..1979bebad98 100644 --- a/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/PlayHttpServerDecorator.java +++ b/dd-java-agent/instrumentation/play-2.6/src/main/java/datadog/trace/instrumentation/play26/PlayHttpServerDecorator.java @@ -168,13 +168,21 @@ private void dispatchRoute(final AgentSpan span, final CharSequence route) { if (ctx == null) { return; } - final CallbackProvider cbp = tracer().getCallbackProvider(RequestContextSlot.APPSEC); - if (cbp == null) { - return; + // Send event to IAST provider + final CallbackProvider cbpIast = tracer().getCallbackProvider(RequestContextSlot.IAST); + if (cbpIast != null) { + final BiConsumer cb = cbpIast.getCallback(EVENTS.httpRoute()); + if (cb != null) { + cb.accept(ctx, URIUtils.decode(route.toString())); + } } - final BiConsumer cb = cbp.getCallback(EVENTS.httpRoute()); - if (cb != null) { - cb.accept(ctx, URIUtils.decode(route.toString())); + // Send event to AppSec provider + final CallbackProvider cbp = tracer().getCallbackProvider(RequestContextSlot.APPSEC); + if (cbp != null) { + final BiConsumer cb = cbp.getCallback(EVENTS.httpRoute()); + if (cb != null) { + cb.accept(ctx, URIUtils.decode(route.toString())); + } } } catch (final Throwable t) { LOG.debug("Failed to dispatch route", t); 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 d85d3468ef2..adbad18dac7 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 @@ -111,6 +111,16 @@ abstract class AbstractIastServerSmokeTest extends AbstractServerSmokeTest { } } + protected void hasMeta(final String name) { + try { + waitForSpan(pollingConditions()) { span -> + return span.meta.containsKey(name) + } + } catch (SpockTimeoutError toe) { + throw new AssertionError("No matching meta with name $name found") + } + } + protected void hasVulnerability(@ClosureParams(value = SimpleType, options = ['datadog.smoketest.model.Vulnerability']) final Closure matcher) { final found = [] diff --git a/dd-smoke-tests/play-2.4/app/controllers/IastController.scala b/dd-smoke-tests/play-2.4/app/controllers/IastController.scala new file mode 100644 index 00000000000..e94fb95a8d9 --- /dev/null +++ b/dd-smoke-tests/play-2.4/app/controllers/IastController.scala @@ -0,0 +1,48 @@ +package controllers + +import play.api.mvc._ + +import java.nio.charset.StandardCharsets +import java.security.MessageDigest + +class IastController extends Controller { + + def multipleVulns(id: String): Action[AnyContent] = Action { + try { + MessageDigest.getInstance("SHA1").digest("hash1".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("SHA-1").digest("hash2".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD2").digest("hash3".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD5").digest("hash4".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("RIPEMD128").digest("hash5".getBytes(StandardCharsets.UTF_8)) + Ok("ok") + } catch { + case e: Exception => InternalServerError(e.getMessage) + } + } + + def postMultipleVulns(id: String): Action[AnyContent] = Action { + try { + MessageDigest.getInstance("SHA1").digest("hash1".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("SHA-1").digest("hash2".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD2").digest("hash3".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD5").digest("hash4".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("RIPEMD128").digest("hash5".getBytes(StandardCharsets.UTF_8)) + Ok("ok") + } catch { + case e: Exception => InternalServerError(e.getMessage) + } + } + + def multipleVulns2(id: String): Action[AnyContent] = Action { + try { + MessageDigest.getInstance("SHA1").digest("hash1".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("SHA-1").digest("hash2".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD2").digest("hash3".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD5").digest("hash4".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("RIPEMD128").digest("hash5".getBytes(StandardCharsets.UTF_8)) + Ok("ok") + } catch { + case e: Exception => InternalServerError(e.getMessage) + } + } +} diff --git a/dd-smoke-tests/play-2.4/build.gradle b/dd-smoke-tests/play-2.4/build.gradle index 13d374d2a24..648246b815e 100644 --- a/dd-smoke-tests/play-2.4/build.gradle +++ b/dd-smoke-tests/play-2.4/build.gradle @@ -64,6 +64,7 @@ dependencies { testImplementation project(':dd-smoke-tests') testImplementation project(':dd-smoke-tests:appsec') + testImplementation(testFixtures(project(":dd-smoke-tests:iast-util"))) } configurations.testImplementation { diff --git a/dd-smoke-tests/play-2.4/conf/routes b/dd-smoke-tests/play-2.4/conf/routes index 0520cfe1842..d557afc17e7 100644 --- a/dd-smoke-tests/play-2.4/conf/routes +++ b/dd-smoke-tests/play-2.4/conf/routes @@ -9,3 +9,8 @@ GET /welcomes controllers.SController.doGet(id: Op # AppSec endpoints for testing GET /api_security/sampling/:statusCode controllers.AppSecController.apiSecuritySampling(statusCode: Int, test: String) + +# IAST Sampling endpoints +GET /iast/multiple_vulns/:id controllers.IastController.multipleVulns(id: String) +POST /iast/multiple_vulns/:id controllers.IastController.postMultipleVulns(id: String) +GET /iast/multiple_vulns-2/:id controllers.IastController.multipleVulns2(id: String) diff --git a/dd-smoke-tests/play-2.4/src/test/groovy/datadog/smoketest/IastPlayNettySmokeTest.groovy b/dd-smoke-tests/play-2.4/src/test/groovy/datadog/smoketest/IastPlayNettySmokeTest.groovy new file mode 100644 index 00000000000..e213493dbb6 --- /dev/null +++ b/dd-smoke-tests/play-2.4/src/test/groovy/datadog/smoketest/IastPlayNettySmokeTest.groovy @@ -0,0 +1,119 @@ +package datadog.smoketest + +import static java.util.concurrent.TimeUnit.SECONDS +import okhttp3.FormBody +import okhttp3.Request +import spock.lang.Shared + +import java.nio.file.Files + + +class IastPlayNettySmokeTest extends AbstractIastServerSmokeTest { + + @Shared + File playDirectory = new File("${buildDirectory}/stage/main") + + @Shared + protected String[] defaultIastProperties = [ + "-Ddd.iast.enabled=true", + "-Ddd.iast.detection.mode=DEFAULT", + "-Ddd.iast.debug.enabled=true", + "-Ddd.iast.request-sampling=100", + ] + + @Override + ProcessBuilder createProcessBuilder() { + // If the server is not shut down correctly, this file can be left there and will block + // the start of a new test + def runningPid = new File(playDirectory.getPath(), "RUNNING_PID") + if (runningPid.exists()) { + runningPid.delete() + } + def command = isWindows() ? 'main.bat' : 'main' + ProcessBuilder processBuilder = new ProcessBuilder("${playDirectory}/bin/${command}") + processBuilder.directory(playDirectory) + processBuilder.environment().put("JAVA_OPTS", + (defaultIastProperties + defaultJavaProperties).collect({ it.replace(' ', '\\ ')}).join(" ") + + " -Dconfig.file=${playDirectory}/conf/application.conf" + + " -Dhttp.port=${httpPort}" + + " -Dhttp.address=127.0.0.1" + + " -Dplay.server.provider=play.core.server.NettyServerProvider" + + " -Ddd.writer.type=MultiWriter:TraceStructureWriter:${output.getAbsolutePath()},DDAgentWriter") + return processBuilder + } + + @Override + File createTemporaryFile() { + return new File("${buildDirectory}/tmp/trace-structure-play-2.4-iast-netty.out") + } + + void 'Test that all the vulnerabilities are detected'() { + given: + def requests = [] + for (int i = 1; i <= 3; i++) { + requests.add(new Request.Builder() + .url("http://localhost:${httpPort}/iast/multiple_vulns/${i}?param=value${i}") + .get() + .build()) + requests.add(new Request.Builder() + .url("http://localhost:${httpPort}/iast/multiple_vulns-2/${i}?param=value${i}") + .get() + .build()) + requests.add(new Request.Builder() + .url("http://localhost:${httpPort}/iast/multiple_vulns/${i}") + .post(new FormBody.Builder().add('param', "value${i}").build()) + .build()) + } + + + when: + requests.each { req -> + client.newCall(req as Request).execute() + } + + then: 'check has route dispatched' + hasMeta('http.route') + + then: 'check first get mapping' + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$multipleVulns$1' && vul.evidence.value == 'SHA1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$multipleVulns$1' && vul.evidence.value == 'SHA-1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$multipleVulns$1' && vul.evidence.value == 'MD2'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$multipleVulns$1' && vul.evidence.value == 'MD5'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$multipleVulns$1' && vul.evidence.value == 'RIPEMD128'} + + then: 'check first post mapping' + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$postMultipleVulns$1' && vul.evidence.value == 'SHA1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$postMultipleVulns$1' && vul.evidence.value == 'SHA-1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$postMultipleVulns$1' && vul.evidence.value == 'MD2'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$postMultipleVulns$1' && vul.evidence.value == 'MD5'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$postMultipleVulns$1' && vul.evidence.value == 'RIPEMD128'} + + then: 'check second get mapping' + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$multipleVulns2$1' && vul.evidence.value == 'SHA1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$multipleVulns2$1' && vul.evidence.value == 'SHA-1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$multipleVulns2$1' && vul.evidence.value == 'MD2'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$multipleVulns2$1' && vul.evidence.value == 'MD5'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$multipleVulns2$1' && vul.evidence.value == 'RIPEMD128'} + } + + // Ensure to clean up server and not only the shell script that starts it + def cleanupSpec() { + def pid = runningServerPid() + if (pid) { + def commands = isWindows() ? ['taskkill', '/PID', pid, '/T', '/F'] : ['kill', '-9', pid] + new ProcessBuilder(commands).start().waitFor(10, SECONDS) + } + } + + def runningServerPid() { + def runningPid = new File(playDirectory.getPath(), 'RUNNING_PID') + if (runningPid.exists()) { + return Files.lines(runningPid.toPath()).findAny().orElse(null) + } + } + + static isWindows() { + return System.getProperty('os.name').toLowerCase().contains('win') + } + +} diff --git a/dd-smoke-tests/play-2.5/app/controllers/IastController.scala b/dd-smoke-tests/play-2.5/app/controllers/IastController.scala new file mode 100644 index 00000000000..cfe0c2e3189 --- /dev/null +++ b/dd-smoke-tests/play-2.5/app/controllers/IastController.scala @@ -0,0 +1,47 @@ +package controllers + +import play.api.mvc._ +import java.security.MessageDigest +import java.nio.charset.StandardCharsets + +class IastController extends Controller { + + def multipleVulns(id: String): Action[AnyContent] = Action { + try { + MessageDigest.getInstance("SHA1").digest("hash1".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("SHA-1").digest("hash2".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD2").digest("hash3".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD5").digest("hash4".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("RIPEMD128").digest("hash5".getBytes(StandardCharsets.UTF_8)) + Ok("ok") + } catch { + case e: Exception => InternalServerError(e.getMessage) + } + } + + def postMultipleVulns(id: String): Action[AnyContent] = Action { + try { + MessageDigest.getInstance("SHA1").digest("hash1".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("SHA-1").digest("hash2".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD2").digest("hash3".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD5").digest("hash4".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("RIPEMD128").digest("hash5".getBytes(StandardCharsets.UTF_8)) + Ok("ok") + } catch { + case e: Exception => InternalServerError(e.getMessage) + } + } + + def multipleVulns2(id: String): Action[AnyContent] = Action { + try { + MessageDigest.getInstance("SHA1").digest("hash1".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("SHA-1").digest("hash2".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD2").digest("hash3".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD5").digest("hash4".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("RIPEMD128").digest("hash5".getBytes(StandardCharsets.UTF_8)) + Ok("ok") + } catch { + case e: Exception => InternalServerError(e.getMessage) + } + } +} diff --git a/dd-smoke-tests/play-2.5/build.gradle b/dd-smoke-tests/play-2.5/build.gradle index 7e363639b28..aab6591da6b 100644 --- a/dd-smoke-tests/play-2.5/build.gradle +++ b/dd-smoke-tests/play-2.5/build.gradle @@ -66,6 +66,7 @@ dependencies { testImplementation project(':dd-smoke-tests') testImplementation project(':dd-smoke-tests:appsec') + testImplementation(testFixtures(project(":dd-smoke-tests:iast-util"))) } configurations.testImplementation { diff --git a/dd-smoke-tests/play-2.5/conf/routes b/dd-smoke-tests/play-2.5/conf/routes index d7f24a29a24..58215f48dc7 100644 --- a/dd-smoke-tests/play-2.5/conf/routes +++ b/dd-smoke-tests/play-2.5/conf/routes @@ -10,3 +10,9 @@ GET /welcomes controllers.SController.doGet(id: Op # AppSec endpoints for testing GET /api_security/sampling/:statusCode controllers.AppSecController.apiSecuritySampling(statusCode: Int, test: String) POST /api_security/response controllers.AppSecController.apiResponse() + +# IAST Sampling endpoints +GET /iast/multiple_vulns/:id controllers.IastController.multipleVulns(id: String) +POST /iast/multiple_vulns/:id controllers.IastController.postMultipleVulns(id: String) +GET /iast/multiple_vulns-2/:id controllers.IastController.multipleVulns2(id: String) + diff --git a/dd-smoke-tests/play-2.5/src/test/groovy/datadog/smoketest/IastPlayNettySmokeTest.groovy b/dd-smoke-tests/play-2.5/src/test/groovy/datadog/smoketest/IastPlayNettySmokeTest.groovy new file mode 100644 index 00000000000..5cd1c795bbd --- /dev/null +++ b/dd-smoke-tests/play-2.5/src/test/groovy/datadog/smoketest/IastPlayNettySmokeTest.groovy @@ -0,0 +1,119 @@ +package datadog.smoketest + +import static java.util.concurrent.TimeUnit.SECONDS +import okhttp3.FormBody +import okhttp3.Request +import spock.lang.Shared + +import java.nio.file.Files + + +class IastPlayNettySmokeTest extends AbstractIastServerSmokeTest { + + @Shared + File playDirectory = new File("${buildDirectory}/stage/main") + + @Shared + protected String[] defaultIastProperties = [ + "-Ddd.iast.enabled=true", + "-Ddd.iast.detection.mode=DEFAULT", + "-Ddd.iast.debug.enabled=true", + "-Ddd.iast.request-sampling=100", + ] + + @Override + ProcessBuilder createProcessBuilder() { + // If the server is not shut down correctly, this file can be left there and will block + // the start of a new test + def runningPid = new File(playDirectory.getPath(), "RUNNING_PID") + if (runningPid.exists()) { + runningPid.delete() + } + def command = isWindows() ? 'main.bat' : 'main' + ProcessBuilder processBuilder = new ProcessBuilder("${playDirectory}/bin/${command}") + processBuilder.directory(playDirectory) + processBuilder.environment().put("JAVA_OPTS", + (defaultIastProperties + defaultJavaProperties).collect({ it.replace(' ', '\\ ')}).join(" ") + + " -Dconfig.file=${playDirectory}/conf/application.conf" + + " -Dhttp.port=${httpPort}" + + " -Dhttp.address=127.0.0.1" + + " -Dplay.server.provider=play.core.server.NettyServerProvider" + + " -Ddd.writer.type=MultiWriter:TraceStructureWriter:${output.getAbsolutePath()},DDAgentWriter") + return processBuilder + } + + @Override + File createTemporaryFile() { + return new File("${buildDirectory}/tmp/trace-structure-play-2.5-iast-netty.out") + } + + void 'Test that all the vulnerabilities are detected'() { + given: + def requests = [] + for (int i = 1; i <= 3; i++) { + requests.add(new Request.Builder() + .url("http://localhost:${httpPort}/iast/multiple_vulns/${i}?param=value${i}") + .get() + .build()) + requests.add(new Request.Builder() + .url("http://localhost:${httpPort}/iast/multiple_vulns-2/${i}?param=value${i}") + .get() + .build()) + requests.add(new Request.Builder() + .url("http://localhost:${httpPort}/iast/multiple_vulns/${i}") + .post(new FormBody.Builder().add('param', "value${i}").build()) + .build()) + } + + + when: + requests.each { req -> + client.newCall(req as Request).execute() + } + + then: 'check has route dispatched' + hasMeta('http.route') + + then: 'check first get mapping' + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$multipleVulns$1' && vul.evidence.value == 'SHA1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$multipleVulns$1' && vul.evidence.value == 'SHA-1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$multipleVulns$1' && vul.evidence.value == 'MD2'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$multipleVulns$1' && vul.evidence.value == 'MD5'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$multipleVulns$1' && vul.evidence.value == 'RIPEMD128'} + + then: 'check first post mapping' + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$postMultipleVulns$1' && vul.evidence.value == 'SHA1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$postMultipleVulns$1' && vul.evidence.value == 'SHA-1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$postMultipleVulns$1' && vul.evidence.value == 'MD2'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$postMultipleVulns$1' && vul.evidence.value == 'MD5'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$postMultipleVulns$1' && vul.evidence.value == 'RIPEMD128'} + + then: 'check second get mapping' + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$multipleVulns2$1' && vul.evidence.value == 'SHA1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$multipleVulns2$1' && vul.evidence.value == 'SHA-1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$multipleVulns2$1' && vul.evidence.value == 'MD2'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$multipleVulns2$1' && vul.evidence.value == 'MD5'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.path == 'controllers.IastController$$anonfun$multipleVulns2$1' && vul.evidence.value == 'RIPEMD128'} + } + + // Ensure to clean up server and not only the shell script that starts it + def cleanupSpec() { + def pid = runningServerPid() + if (pid) { + def commands = isWindows() ? ['taskkill', '/PID', pid, '/T', '/F'] : ['kill', '-9', pid] + new ProcessBuilder(commands).start().waitFor(10, SECONDS) + } + } + + def runningServerPid() { + def runningPid = new File(playDirectory.getPath(), 'RUNNING_PID') + if (runningPid.exists()) { + return Files.lines(runningPid.toPath()).findAny().orElse(null) + } + } + + static isWindows() { + return System.getProperty('os.name').toLowerCase().contains('win') + } + +} diff --git a/dd-smoke-tests/play-2.6/app/controllers/IastController.scala b/dd-smoke-tests/play-2.6/app/controllers/IastController.scala new file mode 100644 index 00000000000..e94fb95a8d9 --- /dev/null +++ b/dd-smoke-tests/play-2.6/app/controllers/IastController.scala @@ -0,0 +1,48 @@ +package controllers + +import play.api.mvc._ + +import java.nio.charset.StandardCharsets +import java.security.MessageDigest + +class IastController extends Controller { + + def multipleVulns(id: String): Action[AnyContent] = Action { + try { + MessageDigest.getInstance("SHA1").digest("hash1".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("SHA-1").digest("hash2".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD2").digest("hash3".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD5").digest("hash4".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("RIPEMD128").digest("hash5".getBytes(StandardCharsets.UTF_8)) + Ok("ok") + } catch { + case e: Exception => InternalServerError(e.getMessage) + } + } + + def postMultipleVulns(id: String): Action[AnyContent] = Action { + try { + MessageDigest.getInstance("SHA1").digest("hash1".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("SHA-1").digest("hash2".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD2").digest("hash3".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD5").digest("hash4".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("RIPEMD128").digest("hash5".getBytes(StandardCharsets.UTF_8)) + Ok("ok") + } catch { + case e: Exception => InternalServerError(e.getMessage) + } + } + + def multipleVulns2(id: String): Action[AnyContent] = Action { + try { + MessageDigest.getInstance("SHA1").digest("hash1".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("SHA-1").digest("hash2".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD2").digest("hash3".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD5").digest("hash4".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("RIPEMD128").digest("hash5".getBytes(StandardCharsets.UTF_8)) + Ok("ok") + } catch { + case e: Exception => InternalServerError(e.getMessage) + } + } +} diff --git a/dd-smoke-tests/play-2.6/build.gradle b/dd-smoke-tests/play-2.6/build.gradle index 48a6b161a3f..1df31bd2c9d 100644 --- a/dd-smoke-tests/play-2.6/build.gradle +++ b/dd-smoke-tests/play-2.6/build.gradle @@ -66,6 +66,7 @@ dependencies { testImplementation project(':dd-smoke-tests') testImplementation project(':dd-smoke-tests:appsec') + testImplementation(testFixtures(project(":dd-smoke-tests:iast-util"))) } configurations.testImplementation { diff --git a/dd-smoke-tests/play-2.6/conf/routes b/dd-smoke-tests/play-2.6/conf/routes index d7f24a29a24..01f31900923 100644 --- a/dd-smoke-tests/play-2.6/conf/routes +++ b/dd-smoke-tests/play-2.6/conf/routes @@ -10,3 +10,8 @@ GET /welcomes controllers.SController.doGet(id: Op # AppSec endpoints for testing GET /api_security/sampling/:statusCode controllers.AppSecController.apiSecuritySampling(statusCode: Int, test: String) POST /api_security/response controllers.AppSecController.apiResponse() + +# IAST Sampling endpoints +GET /iast/multiple_vulns/:id controllers.IastController.multipleVulns(id: String) +POST /iast/multiple_vulns/:id controllers.IastController.postMultipleVulns(id: String) +GET /iast/multiple_vulns-2/:id controllers.IastController.multipleVulns2(id: String) diff --git a/dd-smoke-tests/play-2.6/src/test/groovy/datadog/smoketest/IastPlaySmokeTest.groovy b/dd-smoke-tests/play-2.6/src/test/groovy/datadog/smoketest/IastPlaySmokeTest.groovy new file mode 100644 index 00000000000..7fc17183d62 --- /dev/null +++ b/dd-smoke-tests/play-2.6/src/test/groovy/datadog/smoketest/IastPlaySmokeTest.groovy @@ -0,0 +1,147 @@ +package datadog.smoketest + +import static java.util.concurrent.TimeUnit.SECONDS +import okhttp3.FormBody +import okhttp3.Request +import spock.lang.Shared + +import java.nio.file.Files + +abstract class IastPlaySmokeTest extends AbstractIastServerSmokeTest { + + @Shared + File playDirectory = new File("${buildDirectory}/stage/main") + + @Shared + protected String[] defaultIastProperties = [ + "-Ddd.iast.enabled=true", + "-Ddd.iast.detection.mode=DEFAULT", + "-Ddd.iast.debug.enabled=true", + "-Ddd.iast.request-sampling=100", + ] + + @Override + ProcessBuilder createProcessBuilder() { + // If the server is not shut down correctly, this file can be left there and will block + // the start of a new test + def runningPid = new File(playDirectory.getPath(), "RUNNING_PID") + if (runningPid.exists()) { + runningPid.delete() + } + def command = isWindows() ? 'main.bat' : 'main' + ProcessBuilder processBuilder = + new ProcessBuilder("${playDirectory}/bin/${command}") + processBuilder.directory(playDirectory) + processBuilder.environment().put("JAVA_OPTS", + (defaultIastProperties + defaultJavaProperties).collect({ it.replace(' ', '\\ ')}).join(" ") + + " -Dconfig.file=${playDirectory}/conf/application.conf" + + " -Dhttp.port=${httpPort}" + + " -Dhttp.address=127.0.0.1" + + " -Dplay.server.provider=${serverProvider()}" + + " -Ddd.writer.type=MultiWriter:TraceStructureWriter:${output.getAbsolutePath()},DDAgentWriter") + return processBuilder + } + + @Override + File createTemporaryFile() { + new File("${buildDirectory}/tmp/trace-structure-play-2.6-iast-${serverProviderName()}.out") + } + + abstract String serverProviderName() + + abstract String serverProvider() + + void 'Test that all the vulnerabilities are detected'() { + given: + def requests = [] + for (int i = 1; i <= 3; i++) { + requests.add(new Request.Builder() + .url("http://localhost:${httpPort}/iast/multiple_vulns/${i}?param=value${i}") + .get() + .build()) + requests.add(new Request.Builder() + .url("http://localhost:${httpPort}/iast/multiple_vulns-2/${i}?param=value${i}") + .get() + .build()) + requests.add(new Request.Builder() + .url("http://localhost:${httpPort}/iast/multiple_vulns/${i}") + .post(new FormBody.Builder().add('param', "value${i}").build()) + .build()) + } + + + when: + requests.each { req -> + client.newCall(req as Request).execute() + } + + then: 'check has route dispatched' + hasMeta('http.route') + + then: 'check first get mapping' + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns$1' && vul.evidence.value == 'SHA1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns$1' && vul.evidence.value == 'SHA-1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns$1' && vul.evidence.value == 'MD2'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns$1' && vul.evidence.value == 'MD5'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns$1' && vul.evidence.value == 'RIPEMD128'} + + then: 'check first post mapping' + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$postMultipleVulns$1' && vul.evidence.value == 'SHA1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$postMultipleVulns$1' && vul.evidence.value == 'SHA-1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$postMultipleVulns$1' && vul.evidence.value == 'MD2'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$postMultipleVulns$1' && vul.evidence.value == 'MD5'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$postMultipleVulns$1' && vul.evidence.value == 'RIPEMD128'} + + then: 'check second get mapping' + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns2$1' && vul.evidence.value == 'SHA1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns2$1' && vul.evidence.value == 'SHA-1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns2$1' && vul.evidence.value == 'MD2'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns2$1' && vul.evidence.value == 'MD5'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns2$1' && vul.evidence.value == 'RIPEMD128'} + } + + // Ensure to clean up server and not only the shell script that starts it + def cleanupSpec() { + def pid = runningServerPid() + if (pid) { + def commands = isWindows() ? ['taskkill', '/PID', pid, '/T', '/F'] : ['kill', '-9', pid] + new ProcessBuilder(commands).start().waitFor(10, SECONDS) + } + } + + def runningServerPid() { + def runningPid = new File(playDirectory.getPath(), 'RUNNING_PID') + if (runningPid.exists()) { + return Files.lines(runningPid.toPath()).findAny().orElse(null) + } + } + + static isWindows() { + return System.getProperty('os.name').toLowerCase().contains("win") + } + + static class Akka extends IastPlaySmokeTest { + + @Override + String serverProviderName() { + return "akka-http" + } + + @Override + String serverProvider() { + return "play.core.server.AkkaHttpServerProvider" + } + } + + static class Netty extends IastPlaySmokeTest { + @Override + String serverProviderName() { + return "netty" + } + + @Override + String serverProvider() { + return "play.core.server.NettyServerProvider" + } + } +} diff --git a/dd-smoke-tests/play-2.7/app/controllers/IastController.scala b/dd-smoke-tests/play-2.7/app/controllers/IastController.scala new file mode 100644 index 00000000000..e94fb95a8d9 --- /dev/null +++ b/dd-smoke-tests/play-2.7/app/controllers/IastController.scala @@ -0,0 +1,48 @@ +package controllers + +import play.api.mvc._ + +import java.nio.charset.StandardCharsets +import java.security.MessageDigest + +class IastController extends Controller { + + def multipleVulns(id: String): Action[AnyContent] = Action { + try { + MessageDigest.getInstance("SHA1").digest("hash1".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("SHA-1").digest("hash2".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD2").digest("hash3".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD5").digest("hash4".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("RIPEMD128").digest("hash5".getBytes(StandardCharsets.UTF_8)) + Ok("ok") + } catch { + case e: Exception => InternalServerError(e.getMessage) + } + } + + def postMultipleVulns(id: String): Action[AnyContent] = Action { + try { + MessageDigest.getInstance("SHA1").digest("hash1".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("SHA-1").digest("hash2".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD2").digest("hash3".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD5").digest("hash4".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("RIPEMD128").digest("hash5".getBytes(StandardCharsets.UTF_8)) + Ok("ok") + } catch { + case e: Exception => InternalServerError(e.getMessage) + } + } + + def multipleVulns2(id: String): Action[AnyContent] = Action { + try { + MessageDigest.getInstance("SHA1").digest("hash1".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("SHA-1").digest("hash2".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD2").digest("hash3".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD5").digest("hash4".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("RIPEMD128").digest("hash5".getBytes(StandardCharsets.UTF_8)) + Ok("ok") + } catch { + case e: Exception => InternalServerError(e.getMessage) + } + } +} diff --git a/dd-smoke-tests/play-2.7/build.gradle b/dd-smoke-tests/play-2.7/build.gradle index eaa36eaaccf..a89aed0fdad 100644 --- a/dd-smoke-tests/play-2.7/build.gradle +++ b/dd-smoke-tests/play-2.7/build.gradle @@ -66,6 +66,7 @@ dependencies { testImplementation project(':dd-smoke-tests') testImplementation project(':dd-smoke-tests:appsec') + testImplementation(testFixtures(project(":dd-smoke-tests:iast-util"))) } configurations.testImplementation { diff --git a/dd-smoke-tests/play-2.7/conf/routes b/dd-smoke-tests/play-2.7/conf/routes index d7f24a29a24..01f31900923 100644 --- a/dd-smoke-tests/play-2.7/conf/routes +++ b/dd-smoke-tests/play-2.7/conf/routes @@ -10,3 +10,8 @@ GET /welcomes controllers.SController.doGet(id: Op # AppSec endpoints for testing GET /api_security/sampling/:statusCode controllers.AppSecController.apiSecuritySampling(statusCode: Int, test: String) POST /api_security/response controllers.AppSecController.apiResponse() + +# IAST Sampling endpoints +GET /iast/multiple_vulns/:id controllers.IastController.multipleVulns(id: String) +POST /iast/multiple_vulns/:id controllers.IastController.postMultipleVulns(id: String) +GET /iast/multiple_vulns-2/:id controllers.IastController.multipleVulns2(id: String) diff --git a/dd-smoke-tests/play-2.7/src/test/groovy/datadog/smoketest/IastPlaySmokeTest.groovy b/dd-smoke-tests/play-2.7/src/test/groovy/datadog/smoketest/IastPlaySmokeTest.groovy new file mode 100644 index 00000000000..c97f9682c02 --- /dev/null +++ b/dd-smoke-tests/play-2.7/src/test/groovy/datadog/smoketest/IastPlaySmokeTest.groovy @@ -0,0 +1,146 @@ +package datadog.smoketest + +import static java.util.concurrent.TimeUnit.SECONDS +import okhttp3.FormBody +import okhttp3.Request +import spock.lang.Shared + +import java.nio.file.Files + +abstract class IastPlaySmokeTest extends AbstractIastServerSmokeTest { + + @Shared + File playDirectory = new File("${buildDirectory}/stage/main") + + @Shared + protected String[] defaultIastProperties = [ + "-Ddd.iast.enabled=true", + "-Ddd.iast.detection.mode=DEFAULT", + "-Ddd.iast.debug.enabled=true", + "-Ddd.iast.request-sampling=100", + ] + + @Override + ProcessBuilder createProcessBuilder() { + // If the server is not shut down correctly, this file can be left there and will block + // the start of a new test + def runningPid = new File(playDirectory.getPath(), "RUNNING_PID") + if (runningPid.exists()) { + runningPid.delete() + } + def command = isWindows() ? 'main.bat' : 'main' + ProcessBuilder processBuilder = + new ProcessBuilder("${playDirectory}/bin/${command}") + processBuilder.directory(playDirectory) + processBuilder.environment().put("JAVA_OPTS", + (defaultIastProperties + defaultJavaProperties).collect({ it.replace(' ', '\\ ')}).join(" ") + + " -Dconfig.file=${playDirectory}/conf/application.conf" + + " -Dhttp.port=${httpPort}" + + " -Dhttp.address=127.0.0.1" + + " -Dplay.server.provider=${serverProvider()}" + + " -Ddd.writer.type=MultiWriter:TraceStructureWriter:${output.getAbsolutePath()},DDAgentWriter") + return processBuilder + } + + @Override + File createTemporaryFile() { + new File("${buildDirectory}/tmp/trace-structure-play-2.7-iast-${serverProviderName()}.out") + } + + abstract String serverProviderName() + + abstract String serverProvider() + + void 'Test that all the vulnerabilities are detected'() { + given: + def requests = [] + for (int i = 1; i <= 3; i++) { + requests.add(new Request.Builder() + .url("http://localhost:${httpPort}/iast/multiple_vulns/${i}?param=value${i}") + .get() + .build()) + requests.add(new Request.Builder() + .url("http://localhost:${httpPort}/iast/multiple_vulns-2/${i}?param=value${i}") + .get() + .build()) + requests.add(new Request.Builder() + .url("http://localhost:${httpPort}/iast/multiple_vulns/${i}") + .post(new FormBody.Builder().add('param', "value${i}").build()) + .build()) + } + + + when: + requests.each { req -> + client.newCall(req as Request).execute() + } + + then: 'check has route dispatched' + hasMeta('http.route') + + then: 'check first get mapping' + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns$1' && vul.evidence.value == 'SHA1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns$1' && vul.evidence.value == 'SHA-1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns$1' && vul.evidence.value == 'MD2'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns$1' && vul.evidence.value == 'MD5'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns$1' && vul.evidence.value == 'RIPEMD128'} + + then: 'check first post mapping' + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$postMultipleVulns$1' && vul.evidence.value == 'SHA1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$postMultipleVulns$1' && vul.evidence.value == 'SHA-1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$postMultipleVulns$1' && vul.evidence.value == 'MD2'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$postMultipleVulns$1' && vul.evidence.value == 'MD5'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$postMultipleVulns$1' && vul.evidence.value == 'RIPEMD128'} + + then: 'check second get mapping' + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns2$1' && vul.evidence.value == 'SHA1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns2$1' && vul.evidence.value == 'SHA-1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns2$1' && vul.evidence.value == 'MD2'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns2$1' && vul.evidence.value == 'MD5'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns2$1' && vul.evidence.value == 'RIPEMD128'}} + + // Ensure to clean up server and not only the shell script that starts it + def cleanupSpec() { + def pid = runningServerPid() + if (pid) { + def commands = isWindows() ? ['taskkill', '/PID', pid, '/T', '/F'] : ['kill', '-9', pid] + new ProcessBuilder(commands).start().waitFor(10, SECONDS) + } + } + + def runningServerPid() { + def runningPid = new File(playDirectory.getPath(), 'RUNNING_PID') + if (runningPid.exists()) { + return Files.lines(runningPid.toPath()).findAny().orElse(null) + } + } + + static isWindows() { + return System.getProperty('os.name').toLowerCase().contains("win") + } + + static class Akka extends IastPlaySmokeTest { + + @Override + String serverProviderName() { + return "akka-http" + } + + @Override + String serverProvider() { + return "play.core.server.AkkaHttpServerProvider" + } + } + + static class Netty extends IastPlaySmokeTest { + @Override + String serverProviderName() { + return "netty" + } + + @Override + String serverProvider() { + return "play.core.server.NettyServerProvider" + } + } +} diff --git a/dd-smoke-tests/play-2.8/app/controllers/IastController.scala b/dd-smoke-tests/play-2.8/app/controllers/IastController.scala new file mode 100644 index 00000000000..194405d439c --- /dev/null +++ b/dd-smoke-tests/play-2.8/app/controllers/IastController.scala @@ -0,0 +1,50 @@ +package controllers + +import play.api.mvc._ + +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import javax.inject.{Inject, Singleton} + +@Singleton +class IastController @Inject() (cc: ControllerComponents) extends AbstractController(cc) { + + def multipleVulns(id: String): Action[AnyContent] = Action { + try { + MessageDigest.getInstance("SHA1").digest("hash1".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("SHA-1").digest("hash2".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD2").digest("hash3".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD5").digest("hash4".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("RIPEMD128").digest("hash5".getBytes(StandardCharsets.UTF_8)) + Ok("ok") + } catch { + case e: Exception => InternalServerError(e.getMessage) + } + } + + def postMultipleVulns(id: String): Action[AnyContent] = Action { + try { + MessageDigest.getInstance("SHA1").digest("hash1".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("SHA-1").digest("hash2".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD2").digest("hash3".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD5").digest("hash4".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("RIPEMD128").digest("hash5".getBytes(StandardCharsets.UTF_8)) + Ok("ok") + } catch { + case e: Exception => InternalServerError(e.getMessage) + } + } + + def multipleVulns2(id: String): Action[AnyContent] = Action { + try { + MessageDigest.getInstance("SHA1").digest("hash1".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("SHA-1").digest("hash2".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD2").digest("hash3".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("MD5").digest("hash4".getBytes(StandardCharsets.UTF_8)) + MessageDigest.getInstance("RIPEMD128").digest("hash5".getBytes(StandardCharsets.UTF_8)) + Ok("ok") + } catch { + case e: Exception => InternalServerError(e.getMessage) + } + } +} diff --git a/dd-smoke-tests/play-2.8/build.gradle b/dd-smoke-tests/play-2.8/build.gradle index 60381e29daa..3a114595721 100644 --- a/dd-smoke-tests/play-2.8/build.gradle +++ b/dd-smoke-tests/play-2.8/build.gradle @@ -65,6 +65,7 @@ dependencies { testImplementation project(':dd-smoke-tests') testImplementation project(':dd-smoke-tests:appsec') + testImplementation(testFixtures(project(":dd-smoke-tests:iast-util"))) } configurations.testImplementation { diff --git a/dd-smoke-tests/play-2.8/conf/routes b/dd-smoke-tests/play-2.8/conf/routes index d7f24a29a24..01f31900923 100644 --- a/dd-smoke-tests/play-2.8/conf/routes +++ b/dd-smoke-tests/play-2.8/conf/routes @@ -10,3 +10,8 @@ GET /welcomes controllers.SController.doGet(id: Op # AppSec endpoints for testing GET /api_security/sampling/:statusCode controllers.AppSecController.apiSecuritySampling(statusCode: Int, test: String) POST /api_security/response controllers.AppSecController.apiResponse() + +# IAST Sampling endpoints +GET /iast/multiple_vulns/:id controllers.IastController.multipleVulns(id: String) +POST /iast/multiple_vulns/:id controllers.IastController.postMultipleVulns(id: String) +GET /iast/multiple_vulns-2/:id controllers.IastController.multipleVulns2(id: String) diff --git a/dd-smoke-tests/play-2.8/src/test/groovy/datadog/smoketest/IastPlaySmokeTest.groovy b/dd-smoke-tests/play-2.8/src/test/groovy/datadog/smoketest/IastPlaySmokeTest.groovy new file mode 100644 index 00000000000..d518217fae6 --- /dev/null +++ b/dd-smoke-tests/play-2.8/src/test/groovy/datadog/smoketest/IastPlaySmokeTest.groovy @@ -0,0 +1,147 @@ +package datadog.smoketest + +import static java.util.concurrent.TimeUnit.SECONDS +import okhttp3.FormBody +import okhttp3.Request +import spock.lang.Shared + +import java.nio.file.Files + +abstract class IastPlaySmokeTest extends AbstractIastServerSmokeTest { + + @Shared + File playDirectory = new File("${buildDirectory}/stage/main") + + @Shared + protected String[] defaultIastProperties = [ + "-Ddd.iast.enabled=true", + "-Ddd.iast.detection.mode=DEFAULT", + "-Ddd.iast.debug.enabled=true", + "-Ddd.iast.request-sampling=100", + ] + + @Override + ProcessBuilder createProcessBuilder() { + // If the server is not shut down correctly, this file can be left there and will block + // the start of a new test + def runningPid = new File(playDirectory.getPath(), "RUNNING_PID") + if (runningPid.exists()) { + runningPid.delete() + } + def command = isWindows() ? 'main.bat' : 'main' + ProcessBuilder processBuilder = + new ProcessBuilder("${playDirectory}/bin/${command}") + processBuilder.directory(playDirectory) + processBuilder.environment().put("JAVA_OPTS", + (defaultIastProperties + defaultJavaProperties).collect({ it.replace(' ', '\\ ')}).join(" ") + + " -Dconfig.file=${playDirectory}/conf/application.conf" + + " -Dhttp.port=${httpPort}" + + " -Dhttp.address=127.0.0.1" + + " -Dplay.server.provider=${serverProvider()}" + + " -Ddd.writer.type=MultiWriter:TraceStructureWriter:${output.getAbsolutePath()},DDAgentWriter") + return processBuilder + } + + @Override + File createTemporaryFile() { + new File("${buildDirectory}/tmp/trace-structure-play-2.8-iast-${serverProviderName()}.out") + } + + abstract String serverProviderName() + + abstract String serverProvider() + + void 'Test that all the vulnerabilities are detected'() { + given: + def requests = [] + for (int i = 1; i <= 3; i++) { + requests.add(new Request.Builder() + .url("http://localhost:${httpPort}/iast/multiple_vulns/${i}?param=value${i}") + .get() + .build()) + requests.add(new Request.Builder() + .url("http://localhost:${httpPort}/iast/multiple_vulns-2/${i}?param=value${i}") + .get() + .build()) + requests.add(new Request.Builder() + .url("http://localhost:${httpPort}/iast/multiple_vulns/${i}") + .post(new FormBody.Builder().add('param', "value${i}").build()) + .build()) + } + + + when: + requests.each { req -> + client.newCall(req as Request).execute() + } + + then: 'check has route dispatched' + hasMeta('http.route') + + then: 'check first get mapping' + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns$1' && vul.evidence.value == 'SHA1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns$1' && vul.evidence.value == 'SHA-1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns$1' && vul.evidence.value == 'MD2'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns$1' && vul.evidence.value == 'MD5'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns$1' && vul.evidence.value == 'RIPEMD128'} + + then: 'check first post mapping' + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$postMultipleVulns$1' && vul.evidence.value == 'SHA1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$postMultipleVulns$1' && vul.evidence.value == 'SHA-1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$postMultipleVulns$1' && vul.evidence.value == 'MD2'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$postMultipleVulns$1' && vul.evidence.value == 'MD5'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$postMultipleVulns$1' && vul.evidence.value == 'RIPEMD128'} + + then: 'check second get mapping' + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns2$1' && vul.evidence.value == 'SHA1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns2$1' && vul.evidence.value == 'SHA-1' } + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns2$1' && vul.evidence.value == 'MD2'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns2$1' && vul.evidence.value == 'MD5'} + hasVulnerability { vul -> vul.type == 'WEAK_HASH' && vul.location.method == '$anonfun$multipleVulns2$1' && vul.evidence.value == 'RIPEMD128'} + } + + // Ensure to clean up server and not only the shell script that starts it + def cleanupSpec() { + def pid = runningServerPid() + if (pid) { + def commands = isWindows() ? ['taskkill', '/PID', pid, '/T', '/F'] : ['kill', '-9', pid] + new ProcessBuilder(commands).start().waitFor(10, SECONDS) + } + } + + def runningServerPid() { + def runningPid = new File(playDirectory.getPath(), 'RUNNING_PID') + if (runningPid.exists()) { + return Files.lines(runningPid.toPath()).findAny().orElse(null) + } + } + + static isWindows() { + return System.getProperty('os.name').toLowerCase().contains("win") + } + + static class Akka extends IastPlaySmokeTest { + + @Override + String serverProviderName() { + return "akka-http" + } + + @Override + String serverProvider() { + return "play.core.server.AkkaHttpServerProvider" + } + } + + static class Netty extends IastPlaySmokeTest { + @Override + String serverProviderName() { + return "netty" + } + + @Override + String serverProvider() { + return "play.core.server.NettyServerProvider" + } + } +}