Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ current_output.txt
*.tmp
*.bak
*.backup
dependency-reduced-pom.xml

### Maven invoker generated files ###
examples/**/invoker.properties
Expand Down
1 change: 1 addition & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ mvn capslock:check
- **Maven integration** - Runs as part of your build, not a separate CLI
- **Dependency awareness** - Distinguishes between direct, transitive, and optional dependencies
- **Capability locking** - Snapshot your baseline and get alerted when dependencies gain new capabilities
- **Runtime agent** (Experimental) - Monitor and block capabilities at runtime with policy enforcement. See [agent/README.md](agent/README.md)

## How It Differs from Go Capslock

Expand Down
76 changes: 76 additions & 0 deletions agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# JCapsLock Agent

> **Experimental**: This feature is under active development. APIs and behavior may change.

A Java agent for runtime capability monitoring and policy enforcement.
Instruments JDK classes to detect and optionally block capability usage at runtime.

## Usage

### Log-only mode (monitor capabilities)

```bash
java -javaagent:jcapslock-agent.jar -jar myapp.jar
```

Output shows which capabilities are used and the call stack:

```
[CAPSLOCK] CAPABILITY_FILES:
com.example.MyApp.readConfig()
com.example.Main.main()
```

### Policy enforcement mode (block capabilities)

```bash
java -javaagent:jcapslock-agent.jar=policy.yaml -jar myapp.jar
```

When a blocked capability is used, throws `SecurityException`:

```
SecurityException: [CAPSLOCK] CAPABILITY_FILES blocked for com.example.untrusted.MaliciousLib
```

## Policy File Format

```yaml
policies:
- package: com.example.untrusted
blocked:
- CAPABILITY_FILES
- CAPABILITY_NETWORK
- CAPABILITY_EXEC
- package: com.other.lib
blocked:
- CAPABILITY_EXEC
```

Packages are matched by prefix - blocking `com.example` also blocks `com.example.sub`.

## How It Works

1. **Instrumentation**: Uses ASM to instrument JDK methods that represent
capabilities (e.g., `FileInputStream`, `Socket`, `Runtime.exec`)

2. **Stack inspection**: When an instrumented method is called, walks the call
stack to find application code (skipping JDK frames)

3. **Policy check**: If a policy file is provided, checks if any caller package
is blocked for the capability

4. **Logging**: Always logs capability usage with full stack trace to stderr

## Building

```bash
mvn clean package -pl agent -am
```

The shaded JAR is at `agent/target/jcapslock-agent-1.0-SNAPSHOT.jar`.

## Capabilities

Capabilities are loaded from `java-interesting.cm` in the core module.
See the core module for the full list of tracked JDK methods.
113 changes: 113 additions & 0 deletions agent/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.github.serj</groupId>
<artifactId>jcapslock-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<artifactId>jcapslock-agent</artifactId>
<packaging>jar</packaging>

<name>JCapsLock Agent</name>
<description>Java agent for runtime capability monitoring</description>

<dependencies>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-commons</artifactId>
</dependency>
<dependency>
<groupId>com.github.serj</groupId>
<artifactId>jcapslock-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
<!-- JUnit 5 API for extension support (users bring their own JUnit) -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>provided</scope>
</dependency>

<!-- Test dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>com.github.serj.jcapslock.agent.CapslockAgent</Premain-Class>
<Agent-Class>com.github.serj.jcapslock.agent.CapslockAgent</Agent-Class>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<relocations>
<relocation>
<pattern>org.objectweb.asm</pattern>
<shadedPattern>com.github.serj.jcapslock.agent.asm</shadedPattern>
</relocation>
</relocations>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>module-info.class</exclude>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Premain-Class>com.github.serj.jcapslock.agent.CapslockAgent</Premain-Class>
<Agent-Class>com.github.serj.jcapslock.agent.CapslockAgent</Agent-Class>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
</manifestEntries>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package com.github.serj.jcapslock.agent;

import com.github.serj.jcapslock.capability.CapabilityMapper;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.commons.AdviceAdapter;

import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
* ASM-based class transformer that instruments JDK capability methods
* to log when they are called, including the full call stack.
*
* Capability mappings are loaded from java-interesting.cm via CapabilityMapper.
*/
public class CapabilityTransformer implements ClassFileTransformer {

// Map of internal class name -> (method name -> capability name)
private static final Map<String, Map<String, String>> CAPABILITY_METHODS = buildCapabilityMethods();

private static Map<String, Map<String, String>> buildCapabilityMethods() {
Map<String, Map<String, String>> result = new HashMap<>();

CapabilityMapper.getAllMappings().forEach((fullMethod, capabilities) -> {
int lastDot = fullMethod.lastIndexOf('.');
if (lastDot <= 0) return;

String internalClassName = fullMethod.substring(0, lastDot).replace('.', '/');
String methodName = fullMethod.substring(lastDot + 1);
String capabilityName = capabilities.iterator().next().name();

result.computeIfAbsent(internalClassName, k -> new HashMap<>())
.put(methodName, capabilityName);
});

return result;
}

/**
* Get all internal class names that have capability mappings.
*/
public static Set<String> getInstrumentedClassNames() {
return Collections.unmodifiableSet(CAPABILITY_METHODS.keySet());
}

@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (className == null || !CAPABILITY_METHODS.containsKey(className)) {
return null;
}

try {
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
ClassVisitor cv = new CapabilityClassVisitor(cw, className);
cr.accept(cv, ClassReader.EXPAND_FRAMES);
return cw.toByteArray();
} catch (Exception e) {
Log.error("Failed to transform " + className + ": " + e.getMessage());
return null;
}
}

private static class CapabilityClassVisitor extends ClassVisitor {
private final String className;

CapabilityClassVisitor(ClassVisitor cv, String className) {
super(Opcodes.ASM9, cv);
this.className = className;
}

@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
Map<String, String> methods = CAPABILITY_METHODS.get(className);
if (methods != null && methods.containsKey(name)) {
String capability = methods.get(name);
return new CapabilityMethodVisitor(mv, access, name, descriptor, capability);
}
return mv;
}
}

private static class CapabilityMethodVisitor extends AdviceAdapter {
private final String capability;

CapabilityMethodVisitor(MethodVisitor mv, int access, String name, String descriptor,
String capability) {
super(Opcodes.ASM9, mv, access, name, descriptor);
this.capability = capability;
}

@Override
protected void onMethodEnter() {
// Call PolicyChecker.check(capability)
mv.visitLdcInsn(capability);
mv.visitMethodInsn(Opcodes.INVOKESTATIC,
"com/github/serj/jcapslock/agent/PolicyChecker",
"check",
"(Ljava/lang/String;)V",
false);
}
}
}
Loading