Skip to content

feat: add SCA extension for runtime JAR library inventory#1007

Closed
rejirajraghav wants to merge 1 commit intoelastic:mainfrom
rejirajraghav:feature/sca-extension
Closed

feat: add SCA extension for runtime JAR library inventory#1007
rejirajraghav wants to merge 1 commit intoelastic:mainfrom
rejirajraghav:feature/sca-extension

Conversation

@rejirajraghav
Copy link

Adds a new sca-extension module that intercepts every JAR loaded by the JVM and emits one OTel log event per unique library to the co.elastic.otel.sca instrumentation scope.

Key design:

  • SCAExtension implements both AutoConfigurationCustomizerProvider (registers config defaults before SDK init) and AgentListener (starts JarCollectorService after SDK is ready)
  • JarCollectorService uses a ClassFileTransformer that always returns null -- observes only, never modifies bytecode
  • Metadata extracted in priority order: pom.properties > MANIFEST.MF >
    filename pattern; SHA-256 and pURL (pkg:maven/...) computed per JAR
  • Bounded queue + daemon thread keeps class-loading threads unblocked
  • Token-bucket rate limiting (default 10 JARs/s, configurable)
  • Wired into agent via custom/build.gradle.kts as implementation dependency, identical to the inferred-spans pattern

Config keys: elastic.otel.sca.enabled, elastic.otel.sca.skip_temp_jars, elastic.otel.sca.jars_per_second

Adds a new `sca-extension` module that intercepts every JAR loaded by
the JVM and emits one OTel log event per unique library to the
`co.elastic.otel.sca` instrumentation scope.

Key design:
- `SCAExtension` implements both `AutoConfigurationCustomizerProvider`
  (registers config defaults before SDK init) and `AgentListener`
  (starts `JarCollectorService` after SDK is ready)
- `JarCollectorService` uses a `ClassFileTransformer` that always
  returns null -- observes only, never modifies bytecode
- Metadata extracted in priority order: pom.properties > MANIFEST.MF >
  filename pattern; SHA-256 and pURL (pkg:maven/...) computed per JAR
- Bounded queue + daemon thread keeps class-loading threads unblocked
- Token-bucket rate limiting (default 10 JARs/s, configurable)
- Wired into agent via custom/build.gradle.kts as implementation
  dependency, identical to the inferred-spans pattern

Config keys: elastic.otel.sca.enabled, elastic.otel.sca.skip_temp_jars,
elastic.otel.sca.jars_per_second

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rejirajraghav rejirajraghav requested review from a team as code owners March 18, 2026 11:25
@rejirajraghav rejirajraghav marked this pull request as draft March 18, 2026 11:25
@rejirajraghav rejirajraghav marked this pull request as ready for review March 18, 2026 11:26
@rejirajraghav rejirajraghav marked this pull request as draft March 18, 2026 11:26
// Skip temp JARs (e.g. JRuby, Groovy, or Spring Boot's exploded cache)
if (config.isSkipTempJars()) {
String normPath = normalise(jarPath);
if (normPath.startsWith(tmpDir) || normPath.contains("/tmp/")) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium sca/JarCollectorService.java:344

shouldSkip() uses normPath.startsWith(tmpDir) without checking a path boundary, so a JAR at /tmpfoo/app.jar is incorrectly skipped when tmpDir is /tmp. This causes missing SCA events for any dependency installed under a path that merely shares the temp directory prefix. Consider adding a trailing slash to the prefix check so only actual temp directory contents are skipped.

-      if (normPath.startsWith(tmpDir) || normPath.contains("/tmp/")) {
+      if (normPath.startsWith(tmpDir.endsWith("/") ? tmpDir : tmpDir + "/") || normPath.contains("/tmp/")) {
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file sca-extension/src/main/java/co/elastic/otel/sca/JarCollectorService.java around line 344:

`shouldSkip()` uses `normPath.startsWith(tmpDir)` without checking a path boundary, so a JAR at `/tmpfoo/app.jar` is incorrectly skipped when `tmpDir` is `/tmp`. This causes missing SCA events for any dependency installed under a path that merely shares the temp directory prefix. Consider adding a trailing slash to the prefix check so only actual temp directory contents are skipped.

Evidence trail:
sca-extension/src/main/java/co/elastic/otel/sca/JarCollectorService.java:105 (tmpDir field declaration), line 113 (tmpDir initialization without trailing slash), line 344 (startsWith check without path boundary), lines 373-374 (normalise method shows no trailing slash is added). The code at REVIEWED_COMMIT clearly shows the path boundary issue where `/tmpfoo/app.jar`.startsWith(`/tmp`) would return true.

Comment on lines +116 to +126
if (artifactId.isEmpty()) {
Matcher m = FILENAME_VERSION_PATTERN.matcher(baseName);
if (m.matches()) {
artifactId = m.group(1);
if (version.isEmpty()) {
version = m.group(2);
}
}
// Note: when the filename does not match the version pattern we leave artifactId empty
// so that 'title' from the MANIFEST (if present) is preferred over the raw filename below.
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium sca/JarMetadataExtractor.java:116

When Bundle-SymbolicName is present in the manifest but Implementation-Version is missing, artifactId is populated from the bundle name (line 105) but version remains empty. The filename-based version parser at line 116 is then skipped because artifactId is non-empty, so jars like foo-1.2.3.jar with a manifest containing only Bundle-SymbolicName: foo return with an empty version despite the filename containing a valid version. Consider removing the artifactId.isEmpty() guard at line 116 so filename version extraction runs regardless of whether the artifactId was already determined.

-    if (artifactId.isEmpty()) {
+    if (artifactId.isEmpty() || version.isEmpty()) {
       Matcher m = FILENAME_VERSION_PATTERN.matcher(baseName);
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file sca-extension/src/main/java/co/elastic/otel/sca/JarMetadataExtractor.java around lines 116-126:

When `Bundle-SymbolicName` is present in the manifest but `Implementation-Version` is missing, `artifactId` is populated from the bundle name (line 105) but `version` remains empty. The filename-based version parser at line 116 is then skipped because `artifactId` is non-empty, so jars like `foo-1.2.3.jar` with a manifest containing only `Bundle-SymbolicName: foo` return with an empty version despite the filename containing a valid version. Consider removing the `artifactId.isEmpty()` guard at line 116 so filename version extraction runs regardless of whether the artifactId was already determined.

Evidence trail:
sca-extension/src/main/java/co/elastic/otel/sca/JarMetadataExtractor.java lines 95-126 at REVIEWED_COMMIT: Lines 101-106 show artifactId extraction from Bundle-SymbolicName when artifactId.isEmpty(). Line 116 shows `if (artifactId.isEmpty())` guard. Lines 117-122 show filename pattern matching and version extraction (line 120) only happens inside this guard. This confirms the defect: version extraction from filename is skipped when artifactId was already populated from Bundle-SymbolicName.

Comment on lines +218 to +226
// jar:file:/path/to/outer.jar!/ — nested JAR (Spring Boot, etc.)
if ("jar".equals(location.getProtocol())) {
String path = location.getPath(); // file:/path/to/outer.jar!/
int bang = path.indexOf('!');
if (bang >= 0) {
path = path.substring(0, bang);
}
return new File(new URI(path)).getAbsolutePath();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High sca/JarCollectorService.java:218

locationToJarPath() truncates at the first ! for nested JAR URLs like jar:file:/app.jar!/BOOT-INF/lib/log4j-api-2.22.jar!/, returning /app.jar instead of the nested path. Since seenJarPaths deduplicates on this outer path, all nested libraries from a Spring Boot fat JAR collapse into one record and most dependencies are never reported. Consider preserving the full nested JAR path up to the last !.

-      if ("jar".equals(location.getProtocol())) {
-        String path = location.getPath(); // file:/path/to/outer.jar!/
-        int bang = path.indexOf('!');
-        if (bang >= 0) {
-          path = path.substring(0, bang);
-        }
-        return new File(new URI(path)).getAbsolutePath();
-      }
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file sca-extension/src/main/java/co/elastic/otel/sca/JarCollectorService.java around lines 218-226:

`locationToJarPath()` truncates at the first `!` for nested JAR URLs like `jar:file:/app.jar!/BOOT-INF/lib/log4j-api-2.22.jar!/`, returning `/app.jar` instead of the nested path. Since `seenJarPaths` deduplicates on this outer path, all nested libraries from a Spring Boot fat JAR collapse into one record and most dependencies are never reported. Consider preserving the full nested JAR path up to the last `!`.

Evidence trail:
sca-extension/src/main/java/co/elastic/otel/sca/JarCollectorService.java lines 218-227 (REVIEWED_COMMIT): `locationToJarPath()` uses `path.indexOf('!')` and `path.substring(0, bang)` to truncate at first `!`. Lines 195-198: `seenJarPaths.add(jarPath)` deduplicates on the returned path, causing early return for already-seen paths.

Comment on lines +358 to +371
private static String resolveAgentJarPath() {
String path = System.getProperty("elastic.otel.agent.jar.path");
if (path != null) {
return normalise(path);
}
// Fallback: parse -javaagent flag from the JVM command line
String cmd = System.getProperty("sun.java.command", "");
for (String token : cmd.split("\\s+")) {
if (token.contains("elastic-otel-javaagent") || token.contains("opentelemetry-javaagent")) {
return normalise(token);
}
}
return null;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low sca/JarCollectorService.java:358

resolveAgentJarPath() normalizes the returned path to / separators, but shouldSkip() compares it against paths from File.getAbsolutePath() which use platform separators. On Windows, elastic.otel.agent.jar.path=C:\agent\agent.jar becomes C:/agent/agent.jar while the runtime jarPath stays C:\agent\agent.jar, so the equality check fails and the agent JAR is not excluded.

  private static String resolveAgentJarPath() {
     String path = System.getProperty("elastic.otel.agent.jar.path");
     if (path != null) {
-      return normalise(path);
+      return new File(path).getAbsolutePath();
     }
     // Fallback: parse -javaagent flag from the JVM command line
     String cmd = System.getProperty("sun.java.command", "");
     for (String token : cmd.split("\\s+")) {
       if (token.contains("elastic-otel-javaagent") || token.contains("opentelemetry-javaagent")) {
-        return normalise(token);
+        return new File(token).getAbsolutePath();
       }
     }
     return null;
   }
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file sca-extension/src/main/java/co/elastic/otel/sca/JarCollectorService.java around lines 358-371:

`resolveAgentJarPath()` normalizes the returned path to `/` separators, but `shouldSkip()` compares it against paths from `File.getAbsolutePath()` which use platform separators. On Windows, `elastic.otel.agent.jar.path=C:\agent\agent.jar` becomes `C:/agent/agent.jar` while the runtime `jarPath` stays `C:\agent\agent.jar`, so the equality check fails and the agent JAR is not excluded.

Evidence trail:
JarCollectorService.java lines 358-379: `resolveAgentJarPath()` uses `normalise(path)` which replaces `\` with `/`.
JarCollectorService.java line 216, 225: `locationToJarPath()` uses `new File(...).getAbsolutePath()` which returns platform-native separators.
JarCollectorService.java line 338: `shouldSkip()` compares `agentJarPath.equals(jarPath)` - normalized path vs native path.
Commit: REVIEWED_COMMIT

Comment on lines +149 to +167
static Properties findPomProperties(JarFile jar) throws IOException {
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.isDirectory()) {
continue;
}
String name = entry.getName();
// META-INF/maven/{groupId}/{artifactId}/pom.properties
if (name.startsWith("META-INF/maven/") && name.endsWith("/pom.properties")) {
Properties props = new Properties();
try (InputStream in = jar.getInputStream(entry)) {
props.load(in);
}
return props;
}
}
return null;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium sca/JarMetadataExtractor.java:149

An IOException while reading pom.properties escapes findPomProperties and causes extract() to return null, dropping the entire JAR even though MANIFEST.MF or filename parsing could still recover metadata. Consider catching the exception inside findPomProperties and returning null so the fallback sources in extract() are tried.

  static Properties findPomProperties(JarFile jar) throws IOException {
     Enumeration<JarEntry> entries = jar.entries();
     while (entries.hasMoreElements()) {
       JarEntry entry = entries.nextElement();
       if (entry.isDirectory()) {
         continue;
       }
       String name = entry.getName();
       // META-INF/maven/{groupId}/{artifactId}/pom.properties
       if (name.startsWith("META-INF/maven/") && name.endsWith("/pom.properties")) {
         Properties props = new Properties();
         try (InputStream in = jar.getInputStream(entry)) {
           props.load(in);
         }
         return props;
       }
     }
     return null;
   }
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file sca-extension/src/main/java/co/elastic/otel/sca/JarMetadataExtractor.java around lines 149-167:

An `IOException` while reading `pom.properties` escapes `findPomProperties` and causes `extract()` to return `null`, dropping the entire JAR even though `MANIFEST.MF` or filename parsing could still recover metadata. Consider catching the exception inside `findPomProperties` and returning `null` so the fallback sources in `extract()` are tried.

Evidence trail:
sca-extension/src/main/java/co/elastic/otel/sca/JarMetadataExtractor.java:
- Line 138: `static Properties findPomProperties(JarFile jar) throws IOException`
- Lines 149-151: IOException can be thrown by `props.load(in)`
- Line 73: `Properties pomProps = findPomProperties(jar);` inside try block
- Lines 95-98: catch block catches IOException and returns null immediately
- Lines 80-93: MANIFEST.MF fallback is inside the same try block
- Lines 102-112: filename fallback is after try-catch but unreachable due to early return
- Lines 35-40: class Javadoc documents three-source fallback design

Comment on lines +96 to +128
private final LinkedBlockingQueue<PendingJar> pendingJars =
new LinkedBlockingQueue<>(QUEUE_CAPACITY);

private final AtomicBoolean started = new AtomicBoolean(false);
private final AtomicBoolean stopped = new AtomicBoolean(false);

/** Names/patterns to identify JARs that should never be reported. */
private final String agentJarPath;

private final String tmpDir;

JarCollectorService(
OpenTelemetrySdk openTelemetry, Instrumentation instrumentation, SCAConfiguration config) {
this.openTelemetry = openTelemetry;
this.instrumentation = instrumentation;
this.config = config;
this.agentJarPath = resolveAgentJarPath();
this.tmpDir = normalise(System.getProperty("java.io.tmpdir", "/tmp"));
}

// ---- Lifecycle ---------------------------------------------------------

void start() {
if (!started.compareAndSet(false, true)) {
return;
}

// Register transformer — returns null always, observes only
instrumentation.addTransformer(this, /* canRetransform= */ false);

// Back-fill classes already loaded before our transformer registered
scanAlreadyLoadedClasses();

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High sca/JarCollectorService.java:96

scanAlreadyLoadedClasses() runs in start() before the worker thread begins consuming from pendingJars, so any JVM with more than 500 loaded JARs permanently drops metadata for the excess JARs. Those classes are already loaded, so there will be no retry opportunity later.

     worker.start();
 
     // Drain remaining queue on JVM shutdown before the OTLP exporter shuts down
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file sca-extension/src/main/java/co/elastic/otel/sca/JarCollectorService.java around lines 96-128:

`scanAlreadyLoadedClasses()` runs in `start()` before the worker thread begins consuming from `pendingJars`, so any JVM with more than 500 loaded JARs permanently drops metadata for the excess JARs. Those classes are already loaded, so there will be no retry opportunity later.

Evidence trail:
sca-extension/src/main/java/co/elastic/otel/sca/JarCollectorService.java:
- Line 73: `QUEUE_CAPACITY = 500`
- Lines 105-120: `start()` method - `scanAlreadyLoadedClasses()` called at line 114, worker thread started at line 120
- Lines 170-174: Queue full handling removes from seenJarPaths with comment "will retry on next class load"
- Lines 196-208: `scanAlreadyLoadedClasses()` iterates already loaded classes

Comment on lines +173 to +186
static String buildPurl(String groupId, String artifactId, String version) {
if (artifactId.isEmpty()) {
return "";
}
StringBuilder sb = new StringBuilder("pkg:maven/");
if (!groupId.isEmpty()) {
sb.append(groupId).append('/');
}
sb.append(artifactId);
if (!version.isEmpty()) {
sb.append('@').append(version);
}
return sb.toString();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium sca/JarMetadataExtractor.java:173

buildPurl() returns pkg:maven/{artifactId}@{version} when groupId is empty, producing malformed PURLs like pkg:maven/guava@32.1.3 that omit the required groupId segment. Consider returning "" when groupId is unknown to avoid emitting invalid Maven PURLs.

  static String buildPurl(String groupId, String artifactId, String version) {
-    if (artifactId.isEmpty()) {
+    if (groupId.isEmpty() || artifactId.isEmpty()) {
       return "";
     }
     StringBuilder sb = new StringBuilder("pkg:maven/");
-    if (!groupId.isEmpty()) {
-      sb.append(groupId).append('/');
-    }
-    sb.append(artifactId);
+    sb.append(groupId).append('/').append(artifactId);
     if (!version.isEmpty()) {
       sb.append('@').append(version);
     }
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file sca-extension/src/main/java/co/elastic/otel/sca/JarMetadataExtractor.java around lines 173-186:

`buildPurl()` returns `pkg:maven/{artifactId}@{version}` when `groupId` is empty, producing malformed PURLs like `pkg:maven/guava@32.1.3` that omit the required groupId segment. Consider returning `""` when `groupId` is unknown to avoid emitting invalid Maven PURLs.

Evidence trail:
1. JarMetadataExtractor.java lines 173-186 (REVIEWED_COMMIT): buildPurl() method skips groupId when empty, producing `pkg:maven/artifactId@version`
2. JarMetadataExtractorTest.java lines 128-130 (REVIEWED_COMMIT): Test confirms expected output is `pkg:maven/my-lib@1.0` when groupId is empty
3. https://github.com/package-url/purl-spec/blob/main/docs/types.md - Maven PURL type definition
4. https://github.com/DependencyTrack/dependency-track/issues/2694 - Confirms namespace is required for Maven PURLs

*
* @return loaded {@link Properties}, or {@code null} if not found
*/
static Properties findPomProperties(JarFile jar) throws IOException {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium sca/JarMetadataExtractor.java:149

findPomProperties() returns the first META-INF/maven/**/pom.properties it encounters, but fat JARs contain multiple such files for bundled dependencies. When scanning a fat JAR, the method may return the metadata of an embedded dependency instead of the outer artifact, producing incorrect groupId, artifactId, version, and PURL for the JAR being processed. Consider checking whether the JAR is a fat JAR (e.g., by detecting BOOT-INF/ or multiple pom.properties entries) and handling it appropriately, or document that fat JARs are not supported.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file sca-extension/src/main/java/co/elastic/otel/sca/JarMetadataExtractor.java around line 149:

`findPomProperties()` returns the first `META-INF/maven/**/pom.properties` it encounters, but fat JARs contain multiple such files for bundled dependencies. When scanning a fat JAR, the method may return the metadata of an embedded dependency instead of the outer artifact, producing incorrect `groupId`, `artifactId`, `version`, and PURL for the JAR being processed. Consider checking whether the JAR is a fat JAR (e.g., by detecting `BOOT-INF/` or multiple `pom.properties` entries) and handling it appropriately, or document that fat JARs are not supported.

Evidence trail:
sca-extension/src/main/java/co/elastic/otel/sca/JarMetadataExtractor.java lines 145-167 at REVIEWED_COMMIT: method `findPomProperties()` returns first matching `META-INF/maven/**/pom.properties` entry; git_grep for 'BOOT-INF|maven-shade|fat.*jar|uber.*jar' in sca-extension/** returned no results, confirming no existing fat JAR handling

*/
@Override
public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk) {
SCAConfiguration config = SCAConfiguration.get();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High sca/SCAExtension.java:100

afterAgent() constructs SCAConfiguration.get() from System.getProperty/System.getenv only, ignoring any elastic.otel.sca.* values resolved through the OTel autoconfigure pipeline (SDK config file, ConfigProperties customizers, etc.). A user setting elastic.otel.sca.enabled=false in the OTel config file is silently ignored and JarCollectorService starts anyway.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file sca-extension/src/main/java/co/elastic/otel/sca/SCAExtension.java around line 100:

`afterAgent()` constructs `SCAConfiguration.get()` from `System.getProperty`/`System.getenv` only, ignoring any `elastic.otel.sca.*` values resolved through the OTel autoconfigure pipeline (SDK config file, `ConfigProperties` customizers, etc.). A user setting `elastic.otel.sca.enabled=false` in the OTel config file is silently ignored and `JarCollectorService` starts anyway.

Evidence trail:
- `sca-extension/src/main/java/co/elastic/otel/sca/SCAConfiguration.java` lines 51-55 (`get()` method), lines 67-75 (`readBoolean()` method) - only reads from `System.getProperty()` and `System.getenv()`
- `sca-extension/src/main/java/co/elastic/otel/sca/SCAExtension.java` line 100 (`afterAgent()` calls `SCAConfiguration.get()`)
- `sca-extension/src/main/java/co/elastic/otel/sca/SCAExtension.java` lines 62-71 (`customize()` registers properties with OTel's config pipeline)
- OTel Java Agent Configuration docs at https://opentelemetry.io/docs/zero-code/java/agent/configuration/ confirming config file as a valid configuration source

Comment on lines +295 to +304
private void processJar(PendingJar pending, Logger otelLogger) {
try {
JarMetadata meta = JarMetadataExtractor.extract(pending.jarPath, pending.classloaderName);
if (meta != null) {
emitLogRecord(meta, otelLogger);
}
} catch (Exception e) {
log.log(Level.FINE, "SCA: error processing JAR: " + pending.jarPath, e);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium sca/JarCollectorService.java:295

When JarMetadataExtractor.extract() returns null (e.g., on IOException) or emitLogRecord() throws, processJar() logs and returns without removing pending.jarPath from seenJarPaths. Since the path was already added in enqueueFromProtectionDomain(), future class loads from the same JAR are permanently ignored and the library is never reported. On failure, remove pending.jarPath from seenJarPaths so a later class load can retry.

-  private void processJar(PendingJar pending, Logger otelLogger) {
-    try {
-      JarMetadata meta = JarMetadataExtractor.extract(pending.jarPath, pending.classloaderName);
-      if (meta != null) {
-        emitLogRecord(meta, otelLogger);
-      }
-    } catch (Exception e) {
-      log.log(Level.FINE, "SCA: error processing JAR: " + pending.jarPath, e);
-    }
-  }
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file sca-extension/src/main/java/co/elastic/otel/sca/JarCollectorService.java around lines 295-304:

When `JarMetadataExtractor.extract()` returns `null` (e.g., on `IOException`) or `emitLogRecord()` throws, `processJar()` logs and returns without removing `pending.jarPath` from `seenJarPaths`. Since the path was already added in `enqueueFromProtectionDomain()`, future class loads from the same JAR are permanently ignored and the library is never reported. On failure, remove `pending.jarPath` from `seenJarPaths` so a later class load can retry.

Evidence trail:
JarCollectorService.java lines 169, 174-176 (seenJarPaths.add and conditional removal on queue full), lines 286-296 (processJar method with no seenJarPaths removal on failure). JarMetadataExtractor.java lines 64-67 (returns null if file doesn't exist), lines 100-102 (returns null on IOException).

@coderabbitai
Copy link

coderabbitai bot commented Mar 18, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

This pull request introduces a new SCA (Software Component Analysis) extension module to the Elastic OpenTelemetry Java agent. The extension intercepts class loading to discover JAR files in the runtime, extracts metadata (name, version, group ID, PURL, SHA-256 hash) from various sources (pom.properties, MANIFEST.MF, filename patterns), deduplicates entries, and emits them as OpenTelemetry log records. Configuration is provided via system properties and environment variables. The module is integrated into the build system and registered as a service provider for OpenTelemetry's autoconfig and agent listener mechanisms, with comprehensive test coverage for the metadata extraction logic.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • 🛠️ Update Documentation: Commit on current branch
  • 🛠️ Update Documentation: Create PR
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can make CodeRabbit's review stricter and more nitpicky using the `assertive` profile, if that's what you prefer.

Change the reviews.profile setting to assertive to make CodeRabbit's nitpick more issues in your PRs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant