Gradle
This is the whole path for a Gradle exercise, in the order you have to do it.
Add Ares, label your tests, fill in the checklist, and wire the check into the build so nobody can quietly take it off the desk.
The path, in order
- Add the dependency and the agent, below.
- Set up the public and hidden test model.
- Mark your tests with
@PublicTestor@HiddenTest, and give hidden tests a@Deadline. - Write the policy, choosing one of the four
JAVA_USING_GRADLE_*configurations. - Apply
@Policyto the tests, below. - Reject student classes in reserved packages, below. This step is not optional.
- Verify with both controls, below.
Add the Ares dependency and the agent
Blockquoted tips (marked >) in the setup steps below describe optional configuration that can be
skipped on a first setup. Everything not in a blockquote is required.
First, add the AspectJ compiler plugin:
plugins {
id 'java'
id 'io.freefair.aspectj.post-compile-weaving' version '9.5.0'
}
This plugin runs the AspectJ compiler (ajc) during your build to weave security aspects into bytecode. It provides an aspect dependency configuration; the Ares JAR must be added to it (see Add Ares dependencies) so that ajc treats the aspects shipped inside the Ares JAR as an aspect library and weaves them into your bytecode.
Configure repository lookup
repositories {
mavenCentral()
}
Explanation: mavenCentral() retrieves Ares and its dependencies from Maven Central. Gradle defines no repository by default, so this line is genuinely required. (Maven does define Central in its super-POM, which is why the Maven page has no equivalent step.)
Note: If your
build.gradlealready contains arepositoriesblock, addmavenCentral()to that existing block instead of creating a new one.
Declare the versions once
ext {
aresVersion = '2.1.5'
aspectjVersion = '1.9.25.1'
}
Explanation: Both versions appear in several places below. Declaring each once means an upgrade is a single edit, and it removes the most common upgrade defect, which is changing the dependency coordinate but not the agent path that repeats the same version.
Tip (Gradle version catalog): If your project uses a version catalog (for example
gradle/libs.versions.toml), declare the versions there instead:[versions]ares = "2.1.5"aspectjrt = "1.9.25.1"[libraries]ares = { module = "de.tum.cit.ase:ares", version.ref = "ares" }aspectjrt = { module = "org.aspectj:aspectjrt", version.ref = "aspectjrt" }Then reference
libs.aresandlibs.aspectjrt. Note that version catalogs do not natively support Maven classifiers, so the agent dependency with the:agentclassifier must remain a direct dependency string.
Configure the Ares agent configurations
configurations {
aresAgent {
canBeConsumed = false
canBeResolved = true
transitive = false
}
aresAspectjRuntime {
canBeConsumed = false
canBeResolved = true
transitive = false
}
}
Explanation: A Gradle configuration is a named bucket of dependencies that can be resolved independently. Two dedicated buckets let the build refer to the agent JAR and the AspectJ runtime JAR by their exact file paths in the agent step above, without those files being mixed into the compile or test classpath.
Each property matters:
canBeResolved = trueandcanBeConsumed = false: these buckets are resolved by this build and are not published to other projects.transitive = false: this is what makes the file selection safe. Each bucket then contains exactly one JAR, the one declared for it, so the build can take that file directly. Resolving transitively would pull in the whole dependency graph, and the build would have to guess which file it meant by matching on file names.
The agent JAR contains only Ares's own classes plus the Premain-Class manifest entry; it does not bundle Byte Buddy or any other dependency. Byte Buddy still reaches the instrumented Java Virtual Machine (JVM), because -javaagent attaches to the same JVM that runs your tests, and that JVM's classpath already carries Byte Buddy transitively via the testImplementation dependency in Add Ares dependencies.
Note: If your
build.gradlealready contains aconfigurationsblock, add these to that existing block instead of creating a new one.
Add Ares dependencies
dependencies {
aresAgent "de.tum.cit.ase:ares:${aresVersion}:agent"
aresAspectjRuntime "org.aspectj:aspectjrt:${aspectjVersion}"
testImplementation "de.tum.cit.ase:ares:${aresVersion}"
aspect "de.tum.cit.ase:ares:${aresVersion}"
implementation "org.aspectj:aspectjrt:${aspectjVersion}"
}
Explanation:
aresAgent "de.tum.cit.ase:ares:...:agent": the Ares agent JAR, selected by theagentclassifier. This is the artefact that carries thePremain-Classmanifest entry, so it can be attached with-javaagentas published, with no repackaging.aresAspectjRuntime "org.aspectj:aspectjrt:...": the same AspectJ runtime JAR again, in its own bucket, so that the agent step above can put it on the bootstrap classpath.testImplementation: makes Ares classes available on the test classpath so your test code can use@Policy,@Publicand the other Ares annotations.testImplementationrather thanimplementation, because Ares is needed during testing only. Usingimplementationwould add Ares to the main classpath and the final artefact, where it could interfere with student code, and it violates the principle of minimal dependency scope.aspect "de.tum.cit.ase:ares:...": registers the Ares JAR as an aspect library for the freefair plugin. This is what makesajcweave the binary aspects shipped inside the Ares JAR; atestImplementationdependency alone is not enough, becauseajconly weaves aspects that are on the aspect path (see How compile-time weaving works).implementation "org.aspectj:aspectjrt:...": the AspectJ runtime library, providing classes (for exampleorg.aspectj.lang.JoinPoint) that woven bytecode references.implementationrather thantestImplementation, becauseajcweaves main classes duringcompileJava, and only the main compile classpath is visible at that point.
Note: The freefair plugin manages an
aspectjrtversion of its own. If your build fails with an AspectJ version conflict, drop the explicitimplementation "org.aspectj:aspectjrt:..."line and let the plugin supply it, or align the plugin's version withaspectjVersionthrough itsaspectj { version = aspectjVersion }extension.
Note: If your
build.gradlealready contains adependenciesblock, add these to that existing block instead of creating a new one.
Attach the agent to test execution
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.testing.Test
import org.gradle.process.CommandLineArgumentProvider
abstract class AresJvmArguments implements CommandLineArgumentProvider {
@InputFiles
@PathSensitive(PathSensitivity.NONE)
abstract ConfigurableFileCollection getAgentJar()
@InputFiles
@PathSensitive(PathSensitivity.NONE)
abstract ConfigurableFileCollection getAspectjRuntimeJar()
@Override
Iterable<String> asArguments() {
[
"-javaagent:${agentJar.singleFile.absolutePath}".toString(),
"-Xbootclasspath/a:${aspectjRuntimeJar.singleFile.absolutePath}".toString(),
'--add-exports', 'java.base/java.lang=ALL-UNNAMED',
'--add-exports', 'java.base/jdk.internal.misc=ALL-UNNAMED',
'--add-opens', 'java.base/java.io=ALL-UNNAMED',
'--add-opens', 'java.base/java.lang=ALL-UNNAMED',
'--add-opens', 'java.base/java.lang.reflect=ALL-UNNAMED',
'--add-opens', 'java.base/java.net=ALL-UNNAMED',
'--add-opens', 'java.base/java.nio=ALL-UNNAMED',
'--add-opens', 'java.base/java.nio.channels=ALL-UNNAMED',
'--add-opens', 'java.base/java.util=ALL-UNNAMED',
'--add-opens', 'java.base/java.util.concurrent=ALL-UNNAMED',
'--add-opens', 'java.base/java.util.concurrent.atomic=ALL-UNNAMED',
'--add-opens', 'java.base/java.util.concurrent.locks=ALL-UNNAMED',
'--add-opens', 'java.base/jdk.internal.misc=ALL-UNNAMED',
'--add-opens', 'java.base/sun.net.www.protocol.http=ALL-UNNAMED',
'--add-opens', 'java.base/sun.net.www.protocol.https=ALL-UNNAMED',
'--add-opens', 'java.base/sun.nio.ch=ALL-UNNAMED',
'--add-opens', 'jdk.unsupported/sun.misc=ALL-UNNAMED'
]
}
}
def aresJvmArguments = objects.newInstance(AresJvmArguments)
aresJvmArguments.agentJar.from(configurations.aresAgent)
aresJvmArguments.aspectjRuntimeJar.from(configurations.aresAspectjRuntime)
tasks.withType(Test).configureEach {
useJUnitPlatform()
jvmArgumentProviders.add(aresJvmArguments)
}
Explanation:
- Why an argument provider rather than
jvmArgs. WritingjvmArgs += ["-javaagent:${configurations.aresAgent.singleFile}"]looks simpler, but the string is evaluated while Gradle is configuring the build. That resolves the dependency even when you run an unrelated task, it fails the whole build if resolution fails, and it is incompatible with the configuration cache. ACommandLineArgumentProviderdeclares the JARs as task inputs and computes the arguments when the test task runs. The@InputFilesannotations are what let Gradle track them for the configuration and build caches. - Why
singleFileis safe here. Both configurations aretransitive = falsewith exactly one dependency each, so each resolves to exactly one file. No file-name matching is involved, so there is no way to pick up the wrong JAR. useJUnitPlatform(): enables JUnit 5 (Jupiter) test discovery.-javaagent:...: loads the Ares agent before any user code runs, which is what the instrumentation enforcement path relies on.-Xbootclasspath/a:...: appends the AspectJ runtime JAR to the bootstrap classpath, so woven bytecode can resolve AspectJ runtime types at the bootstrap class-loader level.tasks.withType(Test).configureEach: applies to every test task, including custom ones, rather than only the defaulttesttask.- JVM module access flags. All listed packages must be opened for Ares to introspect intercepted Java Development Kit (JDK) objects and instrument bytecode. The list mirrors the
jvm.module.access.argsproperty in the Arespom.xml:--add-exports java.base/java.lang: makes the publicjava.langapplication programming interface (API) accessible to the unnamed module (test classpath)--add-exports java.base/jdk.internal.misc: makesjdk.internal.misc.Unsafeaccessible to the unnamed module for direct API calls--add-opens java.base/java.lang: allows reflective access to private fields and methods injava.lang(for exampleClass.declaredFields)--add-opens java.base/java.lang.reflect: allows reflective access to private members of the reflection API itself (for exampleField.setAccessible)--add-opens java.base/jdk.internal.misc: allows reflective access to privateUnsafefields (for exampletheUnsafe, read by the thread-system advice)--add-opens jdk.unsupported/sun.misc: allows reflective access to the privatesun.misc.Unsafeinstance (legacyUnsafepath;sun.misclives injdk.unsupported, not injava.base)--add-opens java.base/java.io,java.net,java.nio,java.nio.channels,sun.net.www.protocol.http,sun.net.www.protocol.https,sun.nio.ch: allow the Ares advice to reflectively read fields of intercepted file, network and channel objects (for exampleSocket.delegate)--add-opens java.base/java.util,java.util.concurrent,java.util.concurrent.atomic,java.util.concurrent.locks: allow the Ares advice to reflectively read fields of intercepted collection and concurrency objects (for exampleThreadPoolExecutor.ctl)
How compile-time weaving works
- After compilation: the
io.freefair.aspectj.post-compile-weavingplugin runsajcafterjavacfinishes compiling your source files to bytecode. - Aspect discovery:
ajconly weaves binary aspects from JARs placed on its aspect path; it does not pick them up from the ordinary compile classpath. Theaspectdependency from Add Ares dependencies puts the Ares JAR there. - Weaving:
ajcreads the compiled.classfiles in the project's output directory and weaves in any code whose execution matches the pointcut expressions defined in the discovered aspects. - Runtime references: the woven bytecode references AspectJ runtime classes, supplied by
aspectjrton the bootstrap classpath (configured in the agent step above).
Without the plugin, no weaving occurs and the -Xbootclasspath/a: flag has no effect.
Provide the policy file
Include a security configuration file in your project. A common choice is the project's main directory:
project/
├── src/
├── build.gradle
├── pom.xml
├── secret.txt
├── something.txt
└── SecurityConfiguration.yaml ← placed here
With the following minimal configuration, virtually all actions are denied by default, providing a strict postcompile sandbox for the sample test:
thisPolicyFileCompliesToThePolicyVersion: 1
regardingTheSupervisedCode:
theFollowingProgrammingLanguageConfigurationIsUsed: JAVA_USING_GRADLE_ARCHUNIT_AND_INSTRUMENTATION
theSupervisedCodeUsesTheFollowingPackage: "org.example"
theMainClassInsideThisPackageIs: "Main"
theFollowingClassesAreTestClasses:
- "org.example.PenguinTest"
theFollowingResourceAccessesArePermitted:
regardingFileSystemInteractions:
- onThisPathAndAllPathsBelow: "something.txt"
readAllFiles: true
overwriteAllFiles: true
createAllFiles: true
executeAllFiles: true
deleteAllFiles: true
regardingNetworkConnections: [ ]
regardingCommandExecutions: [ ]
regardingThreadCreations: [ ]
regardingPackageImports: [ ]
regardingTimeouts: [ ]
This permits file system operations only on something.txt. All other paths, including
secret.txt, are not listed and are therefore fully denied by the sandbox.
Annotate the test with @Policy
@Policy links the test to the configuration file and defines the part of the student project
that should be supervised. It does not switch the sandbox on by itself: the Ares test annotation
beneath it is what registers the extensions, and a method carrying a plain JUnit @Test with a
@Policy runs unsupervised without saying so. See
Test Annotations.
@Policy(
value = "SecurityConfiguration.yaml", // path to the configuration file
withinPath = ""
)
@PublicTest
void testPenguinPublic() {
Penguin pingu = new Penguin("Julian");
assertEquals("Julian", pingu.getName(),
"getName() does not return the name supplied to the constructor");
}
With Ares 2 active, the violation now produces:
Ares Security Error (Reason: Student-Code; Stage: Execution): org.example.Penguin.getName tried to illegally overwrite File
/// target file location: secret.txt via java.io.FileOutputStream.write([B,int,int) but was blocked by Ares. ///
Reject student classes in reserved packages
This section is required. Ares trusts a number of runtime identities by name, including its own de.tum.cit.ase.ares.api package and the platform namespaces. If a student can put a class into one of those packages, that class inherits the trust and every other check can be walked around. The build must therefore refuse to compile student output into a reserved package.
The Enforcement Model specifies this boundary and calls it a deployment prerequisite, not an optional Ares runtime feature. Ares ships the executable snippets, so copy them rather than writing your own:
They ship inside the Ares JAR under de/tum/cit/ase/ares/api/configuration/reservedPackages/,
and live in the repository at
src/main/resources/de/tum/cit/ase/ares/api/configuration/reservedPackages/:
GradleReservedPackages.gradleMavenReservedPackages.xmlReservedPackagePrefixes.txt(the machine-readable prefix list)
Two versions are pinned. RESERVED_PACKAGE_PREFIX_VERSION = 1 is the prefix data. RESERVED_PACKAGE_BUILD_BOUNDARY_VERSION = 2 is the build-side contract that enforces it. Your exercise and its continuous integration (CI) must pin both.
Gradle
Apply the shipped GradleReservedPackages.gradle, which registers verifyAresReservedPackagesV2 over sourceSets.main.output.classesDirs and then attaches it in two places:
tasks.named('check') { dependsOn tasks.named('verifyAresReservedPackagesV2') }
tasks.withType(Test).configureEach { dependsOn tasks.named('verifyAresReservedPackagesV2') }
Both are required, and the second is the one that is easy to get wrong. Gradle's Java plugin defines check.dependsOn test, not the reverse. A snippet that hangs the validation off check alone is therefore never executed by gradlew test, which is the command this manual gives you and the command a grading run invokes. That was the defect in boundary version 1: an exercise carrying it accepted student classes in reserved packages. If your exercise still contains a verifyAresReservedPackagesV1 task, migrate it.
In a multi-project build, apply the snippet to every project that compiles student code: tasks.withType(Test) covers only the project it is applied to.
About the forbidden package list
The list above is the versioned reserved-package boundary, and it is deliberately a superset of the canonical Ares list. Besides the packages Ares trusts by name, it stops student code shadowing the test harness itself (JUnit, jqwik, AssertJ, Logback, Gradle).
Keep it aligned with WalaPathClassification.RESERVED_PACKAGE_PREFIX_VERSION (the prefix data)
and RESERVED_PACKAGE_BUILD_BOUNDARY_VERSION (the build-side contract), and do not disable it.
Test task, not onto checkGradle defines check.dependsOn test and not the reverse, so a check-only hook never runs
for gradlew test.
The canonical Maven and Gradle contract is documented in the
enforcement model. The executable snippets ship inside
the Ares JAR and live in the repository at
src/main/resources/de/tum/cit/ase/ares/api/configuration/reservedPackages/,
and complete working exercises are in
examples/
(Maven,
Gradle).
What this boundary does not defend against
The build descriptor and the command that invokes it are trusted instructor configuration. The failure message says "No bypass flag is supported", and that is a statement about the shipped snippets: they offer no opt-out of their own. It is not a claim that the check survives an adversary who controls the build. Anyone who can edit build.gradle or pom.xml, or pass -x verifyAresReservedPackagesV2, can remove the boundary outright.
The threat this addresses is student code that declares a reserved package, not student control over the build. Your exercise template and its CI must own the build files and the invocation, and must fail visibly if either is altered.
Verify your setup
Start from a runnable example
The fastest check is to run something that is already known to work:
cd examples/ares-exercise-gradle && ./gradlew test
cd examples/ares-exercise-maven && mvn test
Each example is a complete exercise: supervised code, a policy, a test class, the reserved-package boundary and the full agent wiring. Each has a README.md stating the exact expected output. If an example passes but your project does not, the difference between the two is your defect.
The two controls that matter
A setup check is only worth running if it can fail for the right reason. The examples are therefore built around a pair of controls in the same domain:
- Positive control: supervised code reads
allowed.txt, which the policy permits. This must succeed. If it fails, enforcement is too strict, or the policy does not say what you think it says. - Negative control: supervised code reads
secret.txt, which the policy does not permit. The test asserts that Ares rejects it. If it succeeds, enforcement is not active at all.
Two details make this a genuine test rather than a reassuring one:
- The forbidden read must happen in supervised code, not in the test. A test class named in
theFollowingClassesAreTestClassesis exempt from enforcement, so a read performed by the test itself is supposed to succeed. Put the read in the student-facing class and let the test assert the exception. - The policy must permit one file in the domain, not zero. This is the part that is easy to get wrong. Ares adds a static deny-all rule only while a domain has no allowance (Enforcement Model). Under a fully restrictive file policy, ArchUnit or T. J. Watson Libraries for Analysis (WALA) rejects the operation before any runtime mechanism is consulted, so the negative control passes even with
-javaagentremoved and the weaving switched off, and it proves nothing. Granting exactly one permitted file makes the runtime layer authoritative for that domain, and only then does the negative control exercise the agent or the woven aspects.
A correct run is therefore green, and contains an asserted rejection. It is not a failed build.
What a green run does and does not prove
A minimal test with no @Policy annotation confirms only that the Ares classes are on the test classpath and that the JVM started with the configured arguments. It does not prove that the agent instrumented anything, and it is not necessarily enforcement-free either: see Further Options for what does and does not happen without a policy.
Prove enforcement with the paired controls from the two controls above, then break the setup deliberately and confirm each break is detected:
- Remove
-javaagentfrom an exercise whose configuration ends in_INSTRUMENTATION. The negative control must now fail to reject. - Remove the
aspectdependency (Gradle) or<aspectLibraries>(Maven) from an exercise whose configuration ends in_ASPECTJ. Same expectation. - Add a class in
de.tum.cit.ase.ares.apito the student sources. The build must fail with the reserved-package diagnostic.
Each break must be made in the mode that depends only on the removed component. Removing -javaagent from an AspectJ exercise changes nothing, because the aspects were woven at compile time and still enforce the policy.
What happens without the agent? Ares's static analysis (ArchUnit/WALA) still works, and AspectJ enforcement still works, because those aspects are woven at compile time. Only the ByteBuddy instrumentation path is inactive, since it relies on the agent to transform classes at load time. If you use an
INSTRUMENTATIONconfiguration, students could then bypass runtime restrictions, so always ensure the agent is loaded.
Appendix: complete build.gradle
The working version of this file, together with the sources and policy it refers to, is examples/ares-exercise-gradle.
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.testing.Test
import org.gradle.process.CommandLineArgumentProvider
plugins {
id 'java'
id 'io.freefair.aspectj.post-compile-weaving' version '9.5.0'
}
ext {
aresVersion = '2.1.5'
aspectjVersion = '1.9.25.1'
}
repositories {
mavenCentral()
}
configurations {
aresAgent {
canBeConsumed = false
canBeResolved = true
transitive = false
}
aresAspectjRuntime {
canBeConsumed = false
canBeResolved = true
transitive = false
}
}
dependencies {
aresAgent "de.tum.cit.ase:ares:${aresVersion}:agent"
aresAspectjRuntime "org.aspectj:aspectjrt:${aspectjVersion}"
testImplementation "de.tum.cit.ase:ares:${aresVersion}"
aspect "de.tum.cit.ase:ares:${aresVersion}"
implementation "org.aspectj:aspectjrt:${aspectjVersion}"
}
abstract class AresJvmArguments implements CommandLineArgumentProvider {
@InputFiles
@PathSensitive(PathSensitivity.NONE)
abstract ConfigurableFileCollection getAgentJar()
@InputFiles
@PathSensitive(PathSensitivity.NONE)
abstract ConfigurableFileCollection getAspectjRuntimeJar()
@Override
Iterable<String> asArguments() {
[
"-javaagent:${agentJar.singleFile.absolutePath}".toString(),
"-Xbootclasspath/a:${aspectjRuntimeJar.singleFile.absolutePath}".toString(),
'--add-exports', 'java.base/java.lang=ALL-UNNAMED',
'--add-exports', 'java.base/jdk.internal.misc=ALL-UNNAMED',
'--add-opens', 'java.base/java.io=ALL-UNNAMED',
'--add-opens', 'java.base/java.lang=ALL-UNNAMED',
'--add-opens', 'java.base/java.lang.reflect=ALL-UNNAMED',
'--add-opens', 'java.base/java.net=ALL-UNNAMED',
'--add-opens', 'java.base/java.nio=ALL-UNNAMED',
'--add-opens', 'java.base/java.nio.channels=ALL-UNNAMED',
'--add-opens', 'java.base/java.util=ALL-UNNAMED',
'--add-opens', 'java.base/java.util.concurrent=ALL-UNNAMED',
'--add-opens', 'java.base/java.util.concurrent.atomic=ALL-UNNAMED',
'--add-opens', 'java.base/java.util.concurrent.locks=ALL-UNNAMED',
'--add-opens', 'java.base/jdk.internal.misc=ALL-UNNAMED',
'--add-opens', 'java.base/sun.net.www.protocol.http=ALL-UNNAMED',
'--add-opens', 'java.base/sun.net.www.protocol.https=ALL-UNNAMED',
'--add-opens', 'java.base/sun.nio.ch=ALL-UNNAMED',
'--add-opens', 'jdk.unsupported/sun.misc=ALL-UNNAMED'
]
}
}
def aresJvmArguments = objects.newInstance(AresJvmArguments)
aresJvmArguments.agentJar.from(configurations.aresAgent)
aresJvmArguments.aspectjRuntimeJar.from(configurations.aresAspectjRuntime)
tasks.withType(Test).configureEach {
useJUnitPlatform()
jvmArgumentProviders.add(aresJvmArguments)
}
// Ares reserved-package build boundary, version 2. See Section 4.
apply from: 'gradle/AresReservedPackages.gradle'