-
Notifications
You must be signed in to change notification settings - Fork 7
Report hashes of jars that we can resolve to dependencies #338
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| package dev.aikido.agent_api.helpers; | ||
|
|
||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import java.security.MessageDigest; | ||
| import java.security.NoSuchAlgorithmException; | ||
|
|
||
| public final class Hashing { | ||
| private static final char[] HEX = "0123456789abcdef".toCharArray(); | ||
|
|
||
| private Hashing() {} | ||
|
|
||
| public static String sha1(InputStream input) throws IOException { | ||
| final MessageDigest digest; | ||
| try { | ||
| digest = MessageDigest.getInstance("SHA-1"); | ||
| } catch (NoSuchAlgorithmException impossible) { | ||
| throw new IllegalStateException("SHA-1 is not available", impossible); | ||
| } | ||
| byte[] buffer = new byte[8192]; | ||
| int read; | ||
| while ((read = input.read(buffer)) != -1) { | ||
| digest.update(buffer, 0, read); | ||
| } | ||
| byte[] hash = digest.digest(); | ||
| char[] encoded = new char[hash.length * 2]; | ||
| for (int i = 0; i < hash.length; i++) { | ||
| int value = hash[i] & 0xff; | ||
| encoded[i * 2] = HEX[value >>> 4]; | ||
| encoded[i * 2 + 1] = HEX[value & 0x0f]; | ||
| } | ||
| return new String(encoded); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| package dev.aikido.agent_api.helpers.packages; | ||
|
|
||
| import dev.aikido.agent_api.helpers.Hashing; | ||
| import dev.aikido.agent_api.storage.RuntimePackage; | ||
|
|
||
| import java.io.BufferedInputStream; | ||
|
|
@@ -29,28 +30,59 @@ private JarPackageScanner() {} | |
| public static List<RuntimePackage> findMavenPackages( | ||
| String classResourceUrl, | ||
| long requiredAt | ||
| ) { | ||
| return scan(classResourceUrl, requiredAt).packages(); | ||
| } | ||
|
|
||
| public static JarScanResult scan( | ||
| String classResourceUrl, | ||
| long requiredAt | ||
| ) { | ||
| try { | ||
| JarLocation location = JarLocation.parse(classResourceUrl); | ||
| if (location == null) { | ||
| return List.of(); | ||
| return JarScanResult.empty(); | ||
| } | ||
| if (location.nestedEntry() == null) { | ||
| List<RuntimePackage> packages; | ||
| try (InputStream input = new BufferedInputStream(Files.newInputStream(location.outerJar()))) { | ||
| return findMavenPackages(input, requiredAt); | ||
| packages = findMavenPackages(input, requiredAt); | ||
| } | ||
| return result(location.outerJar(), packages); | ||
| } | ||
| try (JarFile outerJar = new JarFile(location.outerJar().toFile())) { | ||
| JarEntry nestedJar = outerJar.getJarEntry(location.nestedEntry()); | ||
| if (nestedJar == null) { | ||
| return List.of(); | ||
| return JarScanResult.empty(); | ||
| } | ||
| List<RuntimePackage> packages; | ||
| try (InputStream input = new BufferedInputStream(outerJar.getInputStream(nestedJar))) { | ||
| packages = findMavenPackages(input, requiredAt); | ||
| } | ||
| if (!packages.isEmpty()) { | ||
| return new JarScanResult(packages, null); | ||
| } | ||
| try (InputStream input = new BufferedInputStream(outerJar.getInputStream(nestedJar))) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The nested JAR is read completely once for package scanning and again for SHA-1 calculation. Compute the digest during the initial pass to avoid doubling I/O for unresolved archives. Details✨ AI Reasoning 🔧 How do I fix it? Reply |
||
| return findMavenPackages(input, requiredAt); | ||
| return new JarScanResult(packages, Hashing.sha1(input)); | ||
| } | ||
| } | ||
| } catch (IOException | RuntimeException ignored) { | ||
| return List.of(); | ||
| return JarScanResult.empty(); | ||
| } | ||
| } | ||
|
|
||
| private static JarScanResult result(Path jar, List<RuntimePackage> packages) throws IOException { | ||
| if (!packages.isEmpty()) { | ||
| return new JarScanResult(packages, null); | ||
| } | ||
| try (InputStream input = new BufferedInputStream(Files.newInputStream(jar))) { | ||
| return new JarScanResult(packages, Hashing.sha1(input)); | ||
| } | ||
| } | ||
|
|
||
| public record JarScanResult(List<RuntimePackage> packages, String sha1) { | ||
| private static JarScanResult empty() { | ||
| return new JarScanResult(List.of(), null); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| package dev.aikido.agent_api.storage; | ||
|
|
||
| public record JavaArtifact(String sha1, long requiredAt) {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package dev.aikido.agent_api.storage; | ||
|
|
||
| import java.util.Comparator; | ||
| import java.util.List; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import java.util.concurrent.ConcurrentMap; | ||
|
|
||
| public final class JavaArtifactsStore { | ||
| private static final ConcurrentMap<String, JavaArtifact> ARTIFACTS = new ConcurrentHashMap<>(); | ||
|
|
||
| private JavaArtifactsStore() {} | ||
|
|
||
| public static void add(JavaArtifact artifact) { | ||
| ARTIFACTS.putIfAbsent(artifact.sha1(), artifact); | ||
| } | ||
|
|
||
| public static List<JavaArtifact> getArtifactsAsList() { | ||
| return ARTIFACTS.values().stream() | ||
| .sorted(Comparator.comparing(JavaArtifact::sha1)) | ||
| .toList(); | ||
| } | ||
|
|
||
| public static void clear() { | ||
| ARTIFACTS.clear(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Medium - Artifact hashes are dropped permanently when a heartbeat fails
The new Java artifact inventory is cleared before the heartbeat is sent, so any network error or non-successful heartbeat response discards every collected hash from memory. Those hashes are only queued once per JAR location because observed locations are deduplicated permanently, so the collector will not recreate them after a transient outage. A temporary API failure therefore leaves the backend with a permanently incomplete artifact inventory until the process restarts or a new JAR is loaded.
Show fix
Do not delete
JavaArtifactsStorebefore the heartbeat has been accepted. Instead, drain artifacts atomically only after a successful report, or keep a retry buffer and reinsert unsent artifacts when the heartbeat fails so transient API outages cannot permanently lose the one-time scan results.More info - Reply on this comment to give feedback or ignore the issue.