Skip to content

Commit

Permalink
Appsec support for play 2.5+
Browse files Browse the repository at this point in the history
  • Loading branch information
cataphract committed Oct 2, 2023
1 parent d4ca13f commit 536c674
Show file tree
Hide file tree
Showing 93 changed files with 5,583 additions and 694 deletions.
4 changes: 3 additions & 1 deletion buildSrc/src/main/groovy/InstrumentPlugin.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ abstract class InstrumentTask extends DefaultTask {
parameters.buildStartedTime.set(invocationDetails.buildStartedTime)
parameters.pluginClassPath.setFrom(project.configurations.findByName('instrumentPluginClasspath') ?: [])
parameters.plugins.set(extension.plugins)
parameters.instrumentingClassPath.setFrom(project.configurations.compileClasspath.findAll {
def matcher = instrumentTask.name =~ /instrument([A-Z].+)Java/
def cfgName = matcher.matches() ? "${matcher.group(1).uncapitalize()}CompileClasspath" : 'compileClasspath'
parameters.instrumentingClassPath.setFrom(project.configurations[cfgName].findAll {
it.name != 'previous-compilation-data.bin' && !it.name.endsWith(".gz")
} + sourceDirectory + (extension.additionalClasspath[instrumentTask.name] ?: [])*.get())
parameters.sourceDirectory.set(sourceDirectory.asFile)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,9 @@ class HttpServerDecoratorTest extends ServerDecoratorTest {

where:
rawQuery | rawResource | url | expectedUrl | expectedQuery | expectedResource
false | false | "http://host/p%20ath?query%3F?" | "http://host/p ath" | "query??" | "/path"
false | false | "http://host/p%20ath?query%3F?" | "http://host/p ath" | "query??" | "/p ath"
false | true | "http://host/p%20ath?query%3F?" | "http://host/p%20ath" | "query??" | "/p%20ath"
true | false | "http://host/p%20ath?query%3F?" | "http://host/p ath" | "query%3F?" | "/path"
true | false | "http://host/p%20ath?query%3F?" | "http://host/p ath" | "query%3F?" | "/p ath"
true | true | "http://host/p%20ath?query%3F?" | "http://host/p%20ath" | "query%3F?" | "/p%20ath"

req = [url: url == null ? null : new URI(url)]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package datadog.trace.agent.tooling.bytebuddy.matcher;

import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.is;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.isStatic;
import static net.bytebuddy.matcher.ElementMatchers.not;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;

import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;

public class ScalaTraitMatchers {
public static ElementMatcher.Junction<MethodDescription> isTraitMethod(
String traitName, String name, Object... argumentTypes) {

ElementMatcher.Junction<MethodDescription> scalaOldArgs =
isStatic()
.and(takesArguments(argumentTypes.length + 1))
.and(takesArgument(0, named(traitName)));
ElementMatcher.Junction<MethodDescription> scalaNewArgs =
not(isStatic()).and(takesArguments(argumentTypes.length));

for (int i = 0; i < argumentTypes.length; i++) {
Object argumentType = argumentTypes[i];
ElementMatcher<? super TypeDescription> matcher;
if (argumentType instanceof ElementMatcher) {
matcher = (ElementMatcher<? super TypeDescription>) argumentType;
} else if (argumentType instanceof String) {
matcher = named((String) argumentType);
} else if (argumentType instanceof Class) {
matcher = is((Class<?>) argumentType);
} else {
throw new IllegalArgumentException("Unexpected type for argument type specification");
}
scalaOldArgs = scalaOldArgs.and(takesArgument(i + 1, matcher));
scalaNewArgs = scalaNewArgs.and(takesArgument(i, matcher));
}

return isMethod().and(named(name)).and(scalaOldArgs.or(scalaNewArgs));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import akka.http.scaladsl.model.HttpResponse;
import akka.http.scaladsl.model.ResponseEntity;
import akka.http.scaladsl.model.StatusCode;
import akka.http.scaladsl.model.StatusCodes;
import akka.util.ByteString;
import datadog.appsec.api.blocking.BlockingContentType;
import datadog.trace.api.gateway.BlockResponseFunction;
Expand Down Expand Up @@ -95,7 +96,12 @@ public static HttpResponse maybeCreateBlockingResponse(
RawHeader.create(e.getKey(), e.getValue()))
.collect(ScalaListCollector.toScalaList());

return HttpResponse.apply(
StatusCode.int2StatusCode(httpCode), headersList, entity, request.protocol());
StatusCode code;
try {
code = StatusCode.int2StatusCode(httpCode);
} catch (RuntimeException e) {
code = StatusCodes.custom(httpCode, "Request Blocked", "", false, true);
}
return HttpResponse.apply(code, headersList, entity, request.protocol());
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package datadog.trace.instrumentation.akkahttp.appsec;

import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
import static datadog.trace.instrumentation.akkahttp.iast.TraitMethodMatchers.isTraitMethod;
import static datadog.trace.agent.tooling.bytebuddy.matcher.ScalaTraitMatchers.isTraitMethod;
import static net.bytebuddy.matcher.ElementMatchers.returns;

import akka.http.scaladsl.unmarshalling.MultipartUnmarshallers;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.namedOneOf;
import static datadog.trace.instrumentation.akkahttp.iast.TraitMethodMatchers.isTraitMethod;
import static datadog.trace.agent.tooling.bytebuddy.matcher.ScalaTraitMatchers.isTraitMethod;
import static net.bytebuddy.matcher.ElementMatchers.returns;

import akka.http.scaladsl.unmarshalling.PredefinedFromEntityUnmarshallers;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package datadog.trace.instrumentation.akkahttp.appsec;

import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
import static datadog.trace.instrumentation.akkahttp.iast.TraitMethodMatchers.isTraitMethod;
import static datadog.trace.agent.tooling.bytebuddy.matcher.ScalaTraitMatchers.isTraitMethod;
import static net.bytebuddy.matcher.ElementMatchers.returns;

import akka.http.scaladsl.unmarshalling.Unmarshaller;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,46 +1,15 @@
package datadog.trace.instrumentation.akkahttp.iast;

import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.isStatic;
import static net.bytebuddy.matcher.ElementMatchers.not;
import static datadog.trace.agent.tooling.bytebuddy.matcher.ScalaTraitMatchers.isTraitMethod;
import static net.bytebuddy.matcher.ElementMatchers.returns;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;

import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;

public class TraitMethodMatchers {
public static ElementMatcher.Junction<MethodDescription> isTraitMethod(
String traitName, String name, Object... argumentTypes) {

ElementMatcher.Junction<MethodDescription> scalaOldArgs =
isStatic()
.and(takesArguments(argumentTypes.length + 1))
.and(takesArgument(0, named(traitName)));
ElementMatcher.Junction<MethodDescription> scalaNewArgs =
not(isStatic()).and(takesArguments(argumentTypes.length));

for (int i = 0; i < argumentTypes.length; i++) {
Object argumentType = argumentTypes[i];
ElementMatcher<? super TypeDescription> matcher;
if (argumentType instanceof ElementMatcher) {
matcher = (ElementMatcher<? super TypeDescription>) argumentType;
} else {
matcher = named((String) argumentType);
}
scalaOldArgs = scalaOldArgs.and(takesArgument(i + 1, matcher));
scalaNewArgs = scalaNewArgs.and(takesArgument(i, matcher));
}

return isMethod().and(named(name)).and(scalaOldArgs.or(scalaNewArgs));
}

public static ElementMatcher.Junction<MethodDescription> isTraitDirectiveMethod(
String traitName, String name, String... argumentTypes) {

String traitName, String name, Object... argumentTypes) {
return isTraitMethod(traitName, name, (Object[]) argumentTypes)
.and(returns(named("akka.http.scaladsl.server.Directive")));
}
Expand Down
149 changes: 141 additions & 8 deletions dd-java-agent/instrumentation/play-2.4/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,35 @@ ext {

muzzle {
pass {
name = "play24and25"
group = 'com.typesafe.play'
module = 'play_2.11'
versions = '[2.4.0,2.6)'
assertInverse = true
}
pass {
name = "play25only"
group = 'com.typesafe.play'
module = 'play_2.11'
versions = '[2.5.0,2.6)'
assertInverse = true
}
fail {
name = "play24and25"
group = 'com.typesafe.play'
module = 'play_2.12'
versions = '[,]'
}
fail {
name = "play24and25"
group = 'com.typesafe.play'
module = 'play_2.13'
versions = '[,]'
}
}

apply from: "$rootDir/gradle/java.gradle"
apply plugin: 'scala'

repositories {
maven {
Expand All @@ -33,30 +44,152 @@ repositories {
}
}

tasks.withType(org.gradle.api.tasks.scala.ScalaCompile) {
it.javaLauncher = getJavaLauncherFor(8)
}

addTestSuiteForDir('latestDepTest', 'test')

sourceSets {
main_play25 {
java.srcDirs "${project.projectDir}/src/main/java_play25"
}
}
jar {
from sourceSets.main_play25.output
}
project.afterEvaluate { p ->
instrumentJava.dependsOn compileMain_play25Java
forbiddenApisMain_play25.dependsOn instrumentMain_play25Java
}
instrument {
additionalClasspath = [
instrumentJava: compileMain_play25Java.destinationDirectory
]
}

dependencies {
compileOnly group: 'com.typesafe.play', name: 'play_2.11', version: '2.4.0'
main_play25CompileOnly group: 'com.typesafe.play', name: 'play_2.11', version: '2.5.0'
main_play25CompileOnly project(':internal-api')
main_play25CompileOnly project(':dd-java-agent:agent-tooling')
main_play25CompileOnly project(':dd-java-agent:agent-bootstrap')

testImplementation project(':dd-java-agent:instrumentation:netty-4.0')
testImplementation project(':dd-java-agent:instrumentation:netty-4.1')
testImplementation project(':dd-java-agent:instrumentation:akka-http-10.0')
testImplementation project(':dd-java-agent:instrumentation:akka-concurrent')
testImplementation project(':dd-java-agent:instrumentation:akka-init')
testImplementation project(':dd-java-agent:instrumentation:scala-concurrent')
testImplementation project(':dd-java-agent:instrumentation:scala-promise:scala-promise-2.10')
testImplementation project(':dd-java-agent:instrumentation:scala-promise:scala-promise-2.13')
testRuntimeOnly project(':dd-java-agent:instrumentation:netty-4.0')
testRuntimeOnly project(':dd-java-agent:instrumentation:netty-4.1')
testRuntimeOnly project(':dd-java-agent:instrumentation:akka-http-10.0')
testRuntimeOnly project(':dd-java-agent:instrumentation:akka-concurrent')
testRuntimeOnly project(':dd-java-agent:instrumentation:akka-init')
testRuntimeOnly project(':dd-java-agent:instrumentation:scala-concurrent')
testRuntimeOnly project(':dd-java-agent:instrumentation:scala-promise:scala-promise-2.10')
testRuntimeOnly project(':dd-java-agent:instrumentation:scala-promise:scala-promise-2.13')

// Before 2.5, play used netty 3.x which isn't supported, so for better test consistency, we test with just 2.5
testImplementation group: 'com.typesafe.play', name: 'play-java_2.11', version: '2.5.0'
testImplementation group: 'com.typesafe.play', name: 'play-java-ws_2.11', version: '2.5.0'
testImplementation(group: 'com.typesafe.play', name: 'play-test_2.11', version: '2.5.0') {
exclude group: 'org.eclipse.jetty.websocket', module: 'websocket-client'
}
testRuntimeOnly sourceSets.main_play25.output

latestDepTestImplementation group: 'com.typesafe.play', name: 'play-java_2.11', version: '2.5.+'
latestDepTestImplementation group: 'com.typesafe.play', name: 'play-java-ws_2.11', version: '2.5.+'
latestDepTestImplementation(group: 'com.typesafe.play', name: 'play-test_2.11', version: '2.5.+') {
exclude group: 'org.eclipse.jetty.websocket', module: 'websocket-client'
}
}
compileTestGroovy {
classpath = classpath + files(compileTestScala.destinationDirectory)
dependsOn 'compileTestScala'
}
compileLatestDepTestGroovy {
classpath = classpath + files(compileLatestDepTestScala.destinationDirectory)
dependsOn 'compileLatestDepTestScala'
}

sourceSets {
routeGenerator {
scala {
srcDir "${project.projectDir}/src/routeGenerator/scala"
}
}
testGenerated {
scala {
srcDir layout.buildDirectory.dir('generated/sources/testRoutes/scala')
}
}
latestDepTestGenerated {
scala {
srcDir layout.buildDirectory.dir('generated/sources/latestDepTestRoutes/scala')
}
}
}
dependencies {
routeGeneratorImplementation deps.scala211
routeGeneratorImplementation group: 'com.typesafe.play', name: "routes-compiler_2.11", version: '2.5.0'
}
configurations {
testGeneratedCompileClasspath.extendsFrom testCompileClasspath
latestDepTestGeneratedCompileClasspath.extendsFrom latestDepTestCompileClasspath
}

['buildTestRoutes', 'buildLatestDepTestRoutes'].each { taskName ->
tasks.register(taskName, JavaExec) {
String routesFile = "${project.projectDir}/src/test/routes/conf/routes"
def subdir = taskName == 'buildTestRoutes' ? 'testRoutes' : 'latestDepTestRoutes'
def outputDir =
layout.buildDirectory.dir("generated/sources/$subdir/scala")

it.inputs.file routesFile
it.outputs.dir outputDir

it.mainClass.set 'generator.CompileRoutes'
it.args routesFile, outputDir.get().asFile.absolutePath

it.classpath configurations.routeGeneratorRuntimeClasspath
it.classpath compileRouteGeneratorScala.destinationDirectory

if (taskName == 'buildTestRoutes') {
it.classpath compileTestScala.destinationDirectory
dependsOn compileTestScala
} else {
it.classpath compileLatestDepTestScala.destinationDirectory
dependsOn compileLatestDepTestScala
}

dependsOn compileRouteGeneratorScala
}
}
compileTestGeneratedScala {
classpath = classpath + files(compileTestScala.destinationDirectory)
dependsOn buildTestRoutes, compileLatestDepTestScala
}
compileLatestDepTestGeneratedScala {
classpath = classpath + files(compileLatestDepTestScala.destinationDirectory)
dependsOn buildLatestDepTestRoutes, compileLatestDepTestScala
}
compileTestGroovy {
classpath = classpath +
files(compileTestGeneratedScala.destinationDirectory)
dependsOn 'compileTestGeneratedScala'
}
compileLatestDepTestGroovy {
classpath = classpath +
files(compileLatestDepTestGeneratedScala.destinationDirectory)
dependsOn 'compileLatestDepTestGeneratedScala'
}
// do it this way rather than through dependencies {} because
// latestDepTestImplementation extends testImplementation
test {
classpath = classpath + files(compileTestGeneratedScala.destinationDirectory)
}
latestDepTest {
classpath = classpath + files(compileLatestDepTestGeneratedScala.destinationDirectory)
}

forbiddenApisTestGenerated {
enabled = false
}
forbiddenApisLatestDepTestGenerated {
enabled = false
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ public PlayInstrumentation() {
super("play");
}

@Override
public String muzzleDirective() {
return "play24and25";
}

@Override
public String hierarchyMarkerType() {
return "play.api.mvc.Action";
Expand Down
Loading

0 comments on commit 536c674

Please sign in to comment.