Maven
This is the whole path for a Maven 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_MAVEN_*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.
There is no repository step for Maven: the super-POM already defines Central at https://repo.maven.apache.org/maven2, so re-declaring it adds nothing and can conflict with an organisational mirror.
Declare the versions once
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<ares.version>2.1.5</ares.version>
<aspectj.version>1.9.25.1</aspectj.version>
<!-- Keeps @{argLine} resolvable when JaCoCo is not part of the run. -->
<argLine></argLine>
</properties>
Explanation: the two version properties remove repetition, exactly as in Declare the versions once. The empty argLine property matters for a different reason, explained in the agent step above.
Add Ares dependencies
<dependency>
<groupId>de.tum.cit.ase</groupId>
<artifactId>ares</artifactId>
<version>${ares.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${aspectj.version}</version>
</dependency>
Explanation:
- Ares must be
provided, nottest. This is the single most common way to get a Maven setup that compiles but never enforces anything. Theaspectj-maven-pluginresolves its<aspectLibraries>against the compile-visible project dependencies, and it weaves the main classes duringprocess-classes, where a test-scoped artefact does not exist. A<scope>test</scope>Ares therefore fails the build withThe artifact de.tum.cit.ase:ares referenced in aspectj plugin as an aspect library, is not found the project dependencies.providedscope is visible at compile time and at test time, while still being absent from the packaged artefact and from anything that consumes the exercise transitively, which is exactly the scope an exercise wants. aspectjrtprovides the classes woven bytecode references, and for the same reason it too has no<scope>test</scope>.
Note: If your
pom.xmlalready contains a<dependencies>section, add these to it instead of creating a new one.
Copy the agent and the AspectJ runtime to a known path
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.11.0</version>
<executions>
<execution>
<id>copy-ares-runtime-jars</id>
<phase>process-test-classes</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>de.tum.cit.ase</groupId>
<artifactId>ares</artifactId>
<version>${ares.version}</version>
<classifier>agent</classifier>
<overWrite>true</overWrite>
<outputDirectory>${project.build.directory}/ares</outputDirectory>
<destFileName>ares-agent.jar</destFileName>
</artifactItem>
<artifactItem>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${aspectj.version}</version>
<overWrite>true</overWrite>
<outputDirectory>${project.build.directory}/ares</outputDirectory>
<destFileName>aspectjrt.jar</destFileName>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
Explanation: the agent must be named by an absolute path on the command line, so it has to exist at a path the build knows. Copying both JARs into ${project.build.directory}/ares under fixed names gives that, and it means the version appears only in the properties.
The alternative some guides use, pointing -javaagent at ${settings.localRepository}/de/tum/cit/ase/ares/..., is worse in three ways: it hard-codes the repository layout, it silently assumes the artefact has already been downloaded, and it repeats the version inside a path where an upgrade is easy to miss. process-test-classes runs immediately before test, so the JARs are in place when Surefire starts.
Attach the agent via maven-surefire-plugin
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<argLine>
@{argLine}
"-javaagent:${project.build.directory}/ares/ares-agent.jar"
"-Xbootclasspath/a:${project.build.directory}/ares/aspectjrt.jar"
--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
</argLine>
</configuration>
</plugin>
Explanation:
- Pin the version. Without
<version>, the plugin floats with whatever the super-POM binds, and a Maven upgrade silently changes how your tests are launched. @{argLine}is not optional if anything else contributes Java Virtual Machine (JVM) arguments. JaCoCo'sprepare-agentgoal works by setting theargLineproperty. A plain<argLine>overwrites it, and coverage then silently reports nothing.@{argLine}expands the property late, so both survive. That is why Declare the versions once declares an empty<argLine></argLine>property: without it, a run in which JaCoCo does not participate fails with an unresolved@{argLine}.- Quote the two paths.
${project.build.directory}contains a space whenever the project sits under a directory such asMy Projects. Surefire splitsargLineon whitespace but honours double quotes, so the quotes are what keep such a path in one piece. - Merging with existing arguments. If your exercise already sets
argLine, append these entries to it rather than replacing them, keeping@{argLine}first. If another Java agent is present, order matters: put the Ares agent after a coverage agent such as JaCoCo, so coverage instrumentation is applied to the classes Ares then transforms rather than the reverse. - The module access flags are identical to the Gradle ones; see the agent step above for the per-flag explanation.
Configure AspectJ compile-time weaving
Your build must run the AspectJ compiler to weave the Ares security aspects into student bytecode. Without this step, the -Xbootclasspath/a: flag only provides runtime classes and no weaving ever occurs.
<plugin>
<groupId>dev.aspectj</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.14.1</version>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>${aspectj.version}</version>
</dependency>
</dependencies>
<configuration>
<forceAjcCompile>true</forceAjcCompile>
<complianceLevel>${maven.compiler.source}</complianceLevel>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<aspectLibraries>
<aspectLibrary>
<groupId>de.tum.cit.ase</groupId>
<artifactId>ares</artifactId>
</aspectLibrary>
</aspectLibraries>
</configuration>
<executions>
<execution>
<id>weave-main-classes</id>
<phase>process-classes</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
Explanation:
aspectjtoolsis the AspectJ compiler itself, declared as a plugin dependency so the plugin uses the version from${aspectj.version}.forceAjcCompileforcesajcto run even though the student project contains no.ajsource files. This is essential, because the aspects come from the Ares library rather than from the project.<aspectLibraries>puts the Ares JAR on the aspect path. Without it the plugin runs but weaves nothing, sinceajcdoes not pick up binary aspects from the ordinary compile classpath. The entry references the dependency from the dependency step above, so no version is repeated.- The execution binds
ajctoprocess-classes, immediately afterjavac. Only main classes are woven; test classes are left untouched.
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.
Maven
Apply the shipped MavenReservedPackages.xml, a maven-antrun-plugin execution bound to process-classes that scans ${project.build.outputDirectory}. process-classes precedes test, so mvn test runs it. The Maven binding was already correct at boundary version 1; it carries version 2 only so that both build tools name the same contract.
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 pom.xml
The working version of this file is examples/ares-exercise-maven.
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>ares-exercise</artifactId>
<version>1.0.0</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<ares.version>2.1.5</ares.version>
<aspectj.version>1.9.25.1</aspectj.version>
<!-- Keeps @{argLine} resolvable when JaCoCo is not part of the run. -->
<argLine></argLine>
</properties>
<dependencies>
<!-- provided, not test: see Section 3.2.2. -->
<dependency>
<groupId>de.tum.cit.ase</groupId>
<artifactId>ares</artifactId>
<version>${ares.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${aspectj.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 1. Weave the Ares aspects into the student classes. -->
<plugin>
<groupId>dev.aspectj</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.14.1</version>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>${aspectj.version}</version>
</dependency>
</dependencies>
<configuration>
<forceAjcCompile>true</forceAjcCompile>
<complianceLevel>${maven.compiler.source}</complianceLevel>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<aspectLibraries>
<aspectLibrary>
<groupId>de.tum.cit.ase</groupId>
<artifactId>ares</artifactId>
</aspectLibrary>
</aspectLibraries>
</configuration>
<executions>
<execution>
<id>weave-main-classes</id>
<phase>process-classes</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- 2. Reserved-package build boundary, version 2. See Section 4. -->
<!-- Copy the body of configuration/reservedPackages/MavenReservedPackages.xml here. -->
<!-- 3. Put the agent and the AspectJ runtime at a known path. -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.11.0</version>
<executions>
<execution>
<id>copy-ares-runtime-jars</id>
<phase>process-test-classes</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>de.tum.cit.ase</groupId>
<artifactId>ares</artifactId>
<version>${ares.version}</version>
<classifier>agent</classifier>
<overWrite>true</overWrite>
<outputDirectory>${project.build.directory}/ares</outputDirectory>
<destFileName>ares-agent.jar</destFileName>
</artifactItem>
<artifactItem>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${aspectj.version}</version>
<overWrite>true</overWrite>
<outputDirectory>${project.build.directory}/ares</outputDirectory>
<destFileName>aspectjrt.jar</destFileName>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
<!-- 4. Attach the agent and open the JDK internals Ares introspects. -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<argLine>
@{argLine}
"-javaagent:${project.build.directory}/ares/ares-agent.jar"
"-Xbootclasspath/a:${project.build.directory}/ares/aspectjrt.jar"
--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
</argLine>
</configuration>
</plugin>
</plugins>
</build>
</project>