Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: app support for loading external jars #36

Merged
merged 7 commits into from
Jul 31, 2023
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
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Run it as follows:
<version>latest version here</version> <!-- use latest version here -->
<configuration>
<algorithm>SHA256</algorithm> <!-- optional -->
<externalJars>path to jar</externalJars> <!-- optional -->
</configuration>
<executions>
<execution>
Expand All @@ -56,10 +57,20 @@ themselves.
mvn compile io.github.algomaster99:classfile-fingerprint:generate
```

The plugin also takes an optional `-Dalgorithm` argument to specify the
algorithm used to generate the hash sum. The default is `SHA256`.
Options are
[written here](https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#messagedigest-algorithms).
**Optional parameters**

| Parameter | Type | Description |
|:--------------:|:--------:|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `algorithm` | `String` | Algorithm used to generate the hash sum. Default: `SHA256`.<br/> All options are [written here](https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#messagedigest-algorithms). |
| `externalJars` | `File` | Configuration file to specify external jars. Default: `null`. |

> `externalJars` is a JSON file with the following structure:
> ```json
> [
> {
> "path": "path/to/jar",
> }
> ]

Both methods will output a file `classfile.sha256.jsonl` in the `target` directory.

Expand Down
1 change: 0 additions & 1 deletion classfile-fingerprint/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.24.2</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

import static io.github.algomaster99.terminator.commons.HashComputer.computeHash;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SequenceWriter;
import io.github.algomaster99.terminator.commons.ClassfileVersion;
import io.github.algomaster99.terminator.commons.Fingerprint;
import io.github.algomaster99.terminator.commons.data.ExternalJar;
import io.github.algomaster99.terminator.commons.fingerprint.Fingerprint;
import io.github.algomaster99.terminator.commons.fingerprint.Jar;
import io.github.algomaster99.terminator.commons.fingerprint.Maven;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
Expand Down Expand Up @@ -43,12 +47,19 @@ public class GenerateMojo extends AbstractMojo {
@Parameter(defaultValue = "SHA256", required = true, property = "algorithm")
private String algorithm;

/**
* Path to known external jars.
*/
@Parameter(property = "externalJars")
private File externalJars;

private final List<Fingerprint> fingerprints = new ArrayList<>();

@Override
public void execute() throws MojoExecutionException, MojoFailureException {
processProjectItself();
processDependencies();
processExternalJars();

Path fingerprintFile = getFingerprintFile(project, algorithm);
writeFingerprint(fingerprints, fingerprintFile);
Expand Down Expand Up @@ -94,34 +105,7 @@ private void processDependency(Artifact artifact) {
getLog().debug("Artifact file on system: " + artifactFileOnSystem);

if (artifactFileOnSystem.toString().endsWith(".jar")) {
try (JarFile jarFile = new JarFile(artifactFileOnSystem)) {
Enumeration<JarEntry> jarEntries = jarFile.entries();
while (jarEntries.hasMoreElements()) {
JarEntry jarEntry = jarEntries.nextElement();
if (jarEntry.getName().endsWith(".class")) {
getLog().debug("Found class: " + jarEntry.getName());
byte[] classfileBytes = jarFile.getInputStream(jarEntry).readAllBytes();
String hashOfClass = computeHash(classfileBytes, algorithm);

String jarEntryName = jarEntry.getName()
.substring(0, jarEntry.getName().length() - ".class".length());
String classfileVersion = ClassfileVersion.getVersion(classfileBytes);

fingerprints.add(new Fingerprint(
artifact.getGroupId(),
artifact.getArtifactId(),
artifact.getVersion(),
jarEntryName,
classfileVersion,
hashOfClass,
algorithm));
}
}
} catch (IOException e) {
getLog().error("Could not open JAR file: " + artifactFileOnSystem);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
goInsideJar(artifactFileOnSystem, artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion());
} else {
getLog().debug("Artifact is not a JAR file: " + artifactFileOnSystem);
getLog().debug("Artifact might be in classes directory.");
Expand All @@ -130,6 +114,41 @@ private void processDependency(Artifact artifact) {
}
}

private void goInsideJar(File artifactFileOnSystem, String... provenanceInformation) {
try (JarFile jarFile = new JarFile(artifactFileOnSystem)) {
Enumeration<JarEntry> jarEntries = jarFile.entries();
while (jarEntries.hasMoreElements()) {
JarEntry jarEntry = jarEntries.nextElement();
if (jarEntry.getName().endsWith(".class")) {
getLog().debug("Found class: " + jarEntry.getName());
byte[] classfileBytes = jarFile.getInputStream(jarEntry).readAllBytes();
String hashOfClass = computeHash(classfileBytes, algorithm);

String jarEntryName =
jarEntry.getName().substring(0, jarEntry.getName().length() - ".class".length());
String classfileVersion = ClassfileVersion.getVersion(classfileBytes);

if (provenanceInformation.length == 3) {
String groupId = provenanceInformation[0];
String artifactId = provenanceInformation[1];
String version = provenanceInformation[2];
fingerprints.add(new Maven(
groupId, artifactId, version, jarEntryName, classfileVersion, hashOfClass, algorithm));
} else if (provenanceInformation.length == 1) {
String jarLocation = provenanceInformation[0];
fingerprints.add(new Jar(jarLocation, jarEntryName, classfileVersion, hashOfClass, algorithm));
} else {
throw new RuntimeException("Wrong number of elements in provenance information.");
}
}
}
} catch (IOException e) {
getLog().error("Could not open JAR file: " + artifactFileOnSystem);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}

private void walkOverClassDirectory(File artifactFileOnSystem, String groupId, String artifactId, String version) {
try (Stream<Path> stream = Files.walk(artifactFileOnSystem.toPath())) {
stream.filter(Files::isRegularFile)
Expand All @@ -146,7 +165,7 @@ private void walkOverClassDirectory(File artifactFileOnSystem, String groupId, S
byte[] classfileBytes = byteStream.readAllBytes();
String hashOfClass = computeHash(classfileBytes, algorithm);
String classfileVersion = ClassfileVersion.getVersion(classfileBytes);
fingerprints.add(new Fingerprint(
fingerprints.add(new Maven(
groupId, artifactId, version, className, classfileVersion, hashOfClass, algorithm));
} catch (IOException e) {
getLog().error("Could not open file: " + path);
Expand All @@ -160,6 +179,27 @@ private void walkOverClassDirectory(File artifactFileOnSystem, String groupId, S
}
}

private void processExternalJars() {
if (externalJars == null) {
getLog().info("No external jars are known.");
return;
}

ObjectMapper mapper = new ObjectMapper();
List<ExternalJar> externalJarList;
try {
externalJarList =
mapper.readerFor(new TypeReference<List<ExternalJar>>() {}).readValue(externalJars);
} catch (IOException e) {
throw new RuntimeException("Could not open external jar file: " + e);
}

for (ExternalJar jar : externalJarList) {
getLog().info("Processing external jar" + jar.path().getAbsolutePath());
goInsideJar(jar.path().getAbsoluteFile(), jar.path().getAbsolutePath());
}
}

private static void writeFingerprint(List<Fingerprint> fingerprints, Path fingerprintFile) {
ObjectMapper mapper = new ObjectMapper();
try (SequenceWriter seq =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@
import com.soebes.itf.jupiter.extension.MavenOption;
import com.soebes.itf.jupiter.extension.MavenTest;
import com.soebes.itf.jupiter.maven.MavenExecutionResult;
import io.github.algomaster99.terminator.commons.Fingerprint;
import io.github.algomaster99.terminator.commons.fingerprint.Fingerprint;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;

Expand Down Expand Up @@ -169,7 +170,33 @@ void multi_module_with_sources(MavenExecutionResult result) throws IOException {
assertThat(fingerPrint).isRegularFile().hasContent(Files.readString(expectedFingerprint));
}

@MavenTest
@MavenOption("-DexternalJars=src/test/resources/externalJars.json")
void url_classloader_local_jar(MavenExecutionResult result) {
assertThat(result).isSuccessful();

Path projectDirectory = result.getMavenProjectResult().getTargetProjectDirectory();
Path fingerprint = getFingerprint(projectDirectory, "classfile.sha256.jsonl");
List<Fingerprint> fingerprints = parseFingerprints(fingerprint);

assertThat(fingerprints)
.hasSize(2)
.element(1)
.extracting("className", "hash")
.containsExactly("NonMalicious", "de3318e0ba5527a90fb600307ca12e0d06752474d1da3086cfdb4a48f714da5d");
}

private static Path getFingerprint(Path projectDirectory, String classfileFingerprintName) {
return Path.of(projectDirectory.toString(), "target", classfileFingerprintName);
}

private static List<Fingerprint> parseFingerprints(Path fingerprintFile) {
final ObjectMapper mapper = new ObjectMapper();
try (MappingIterator<Fingerprint> it =
mapper.readerFor(Fingerprint.class).readValues(fingerprintFile.toFile())) {
return it.readAll();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?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>

<groupId>org.example</groupId>
<artifactId>url-classloader</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>15</maven.compiler.source>
<maven.compiler.target>15</maven.compiler.target>
</properties>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<archive>
<manifest>
<mainClass>org.example.LocalFile</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.example;

import java.net.MalformedURLException;
import java.nio.file.Path;
import java.net.URL;
import java.net.URLClassLoader;

public class LocalFile {
public static void main(String[] args) throws MalformedURLException {
Path path = Path.of("external_source", "non-malicious.jar").toAbsolutePath();
URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { path.toUri().toURL() });
try {
Class<?> nonMaliciousClass = urlClassLoader.loadClass("NonMalicious");
nonMaliciousClass.getMethod("main", String[].class).invoke(null, (Object) null);
} catch (ReflectiveOperationException e) {
e.printStackTrace();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
{
"path": "external_source/non-malicious.jar"
}
]
10 changes: 10 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.24.2</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.0</version>
</dependency>
</dependencies>
</dependencyManagement>

Expand Down
15 changes: 15 additions & 0 deletions terminator-commons/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.github.algomaster99.terminator.commons.data;

import java.io.File;

public record ExternalJar(File path) {}
Loading