diff --git a/custom/build.gradle.kts b/custom/build.gradle.kts index a415796a..bf69c0e1 100644 --- a/custom/build.gradle.kts +++ b/custom/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation(libs.dslJson) implementation(libs.okhttp) implementation(project(":inferred-spans")) + implementation(project(":sca-extension")) implementation(project(":universal-profiling-integration")) implementation(project(":resources")) implementation(project(":internal-logging", configuration = "shadow")) diff --git a/sca-extension/README.md b/sca-extension/README.md new file mode 100644 index 00000000..f1015db2 --- /dev/null +++ b/sca-extension/README.md @@ -0,0 +1,85 @@ +# sca-extension — Software Composition Analysis for EDOT Java + +Automatically discovers every JAR loaded by the JVM at runtime, extracts library metadata, and emits one OpenTelemetry log event per unique JAR to Elasticsearch via OTLP. The events land in the `logs-sca-default` data stream where an Elasticsearch ingest pipeline can enrich them with CVE data from the OSV database using an enrich processor that matches on `library.purl`. + +## How it works + +1. **Discovery** — registers a `ClassFileTransformer` with the JVM `Instrumentation` object. For every loaded class the transformer reads `ProtectionDomain.getCodeSource().getLocation()` to obtain the owning JAR path. This never transforms bytecode and never blocks the class-loading thread. + +2. **Back-fill** — on startup, `Instrumentation.getAllLoadedClasses()` is scanned to capture JARs loaded before the transformer registered. + +3. **Deduplication** — a `ConcurrentHashMap` keyed on JAR path ensures each JAR is processed exactly once. + +4. **Async metadata extraction** — new JAR paths are placed in a bounded queue (capacity 500). A single daemon thread reads from the queue, opens each JAR, and extracts metadata using three sources in priority order: + - `META-INF/maven/*/*/pom.properties` — most reliable for `groupId`, `artifactId`, `version` + - `META-INF/MANIFEST.MF` — `Implementation-Title`, `Implementation-Version`, `Specification-Version` + - Filename pattern — `name-version.jar` best-effort parse + +5. **SHA-256 fingerprint** — computed from JAR bytes for exact CVE matching. + +6. **Rate-limited emission** — log records are emitted at a configurable rate (default: 10 JARs/second) using a token-bucket style sleep on the background thread. + +## Emitted log record + +| Field | OTel attribute | Example | +|---|---|---| +| Body | — | `com.google.guava:guava:32.1.3-jre` | +| Library name | `library.name` | `guava` | +| Library version | `library.version` | `32.1.3-jre` | +| Maven groupId | `library.group_id` | `com.google.guava` | +| Package URL | `library.purl` | `pkg:maven/com.google.guava/guava@32.1.3-jre` | +| JAR path | `library.jar_path` | `/opt/app/lib/guava-32.1.3-jre.jar` | +| SHA-256 | `library.sha256` | `a1b2c3...` | +| Classloader | `library.classloader` | `jdk.internal.loader.ClassLoaders$AppClassLoader` | +| Event name | `event.name` | `library.loaded` | +| Event domain | `event.domain` | `sca` | + +The instrumentation scope is `co.elastic.otel.sca`. + +## Configuration + +All properties can be set as JVM system properties or environment variables. + +| System property | Env var | Default | Description | +|---|---|---|---| +| `elastic.otel.sca.enabled` | `ELASTIC_OTEL_SCA_ENABLED` | `true` | Enable / disable the extension | +| `elastic.otel.sca.skip_temp_jars` | `ELASTIC_OTEL_SCA_SKIP_TEMP_JARS` | `true` | Skip JARs under `java.io.tmpdir` (e.g. JRuby, Groovy generated JARs) | +| `elastic.otel.sca.jars_per_second` | `ELASTIC_OTEL_SCA_JARS_PER_SECOND` | `10` | Maximum JAR events emitted per second | + +Example: + +```bash +java -javaagent:elastic-otel-javaagent.jar \ + -Delastic.otel.sca.enabled=true \ + -Delastic.otel.sca.jars_per_second=20 \ + -jar myapp.jar +``` + +## Build & packaging + +The module is built with `elastic-otel.library-packaging-conventions` and is included in the agent as an `implementation` dependency of the `custom` module — the same path taken by `inferred-spans`. No changes to the agent packaging convention are required. + +``` +agent (elastic-otel-javaagent.jar) + └── custom (javaagentLibs) + └── sca-extension (transitive implementation dep) +``` + +The two SPI registrations in `META-INF/services/` are merged into `inst/META-INF/services/` inside the agent JAR by the `mergeServiceFiles()` step in `elastic-otel.agent-packaging-conventions`. + +## Downstream enrichment + +The recommended Elasticsearch ingest pipeline uses an enrich processor that joins `library.purl` with an OSV-sourced enrich index: + +```json +{ + "enrich": { + "policy_name": "osv-cve-by-purl", + "field": "library.purl", + "target_field": "vulnerability", + "ignore_missing": true + } +} +``` + +This adds `vulnerability.cve`, `vulnerability.severity`, and `vulnerability.fix_available` fields to each log document, enabling a full library inventory with CVE status visible in Kibana. diff --git a/sca-extension/build.gradle.kts b/sca-extension/build.gradle.kts new file mode 100644 index 00000000..1ee1a3ab --- /dev/null +++ b/sca-extension/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + id("elastic-otel.library-packaging-conventions") +} + +description = "Elastic SCA (Software Composition Analysis) extension for OpenTelemetry Java" + +tasks.compileJava { + options.encoding = "UTF-8" +} + +tasks.javadoc { + options.encoding = "UTF-8" +} + +dependencies { + compileOnly("io.opentelemetry:opentelemetry-sdk") + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") + compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api") + // AutoConfiguredOpenTelemetrySdk is not yet exposed through the extension API + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + + testImplementation(project(":testing-common")) + testImplementation("io.opentelemetry:opentelemetry-sdk") + testImplementation("io.opentelemetry:opentelemetry-sdk-testing") + testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") +} diff --git a/sca-extension/src/main/java/co/elastic/otel/sca/JarCollectorService.java b/sca-extension/src/main/java/co/elastic/otel/sca/JarCollectorService.java new file mode 100644 index 00000000..dca8b6cb --- /dev/null +++ b/sca-extension/src/main/java/co/elastic/otel/sca/JarCollectorService.java @@ -0,0 +1,389 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.otel.sca; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import java.io.File; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.Instrumentation; +import java.net.URI; +import java.net.URL; +import java.security.CodeSource; +import java.security.ProtectionDomain; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; + +/** + * Core SCA service that intercepts class loading, extracts JAR metadata asynchronously, and emits + * one OTel log event per unique JAR to the {@code co.elastic.otel.sca} instrumentation scope. + * + *
Design constraints: + * + *
System properties take precedence over environment variables. Default values are also + * registered in the OTel autoconfigure pipeline by {@link SCAExtension#customize} so they appear + * in any config-dump tooling that inspects OTel properties. + */ +public final class SCAConfiguration { + + static final String ENABLED_KEY = "elastic.otel.sca.enabled"; + static final String ENABLED_ENV = "ELASTIC_OTEL_SCA_ENABLED"; + + static final String SKIP_TEMP_JARS_KEY = "elastic.otel.sca.skip_temp_jars"; + static final String SKIP_TEMP_JARS_ENV = "ELASTIC_OTEL_SCA_SKIP_TEMP_JARS"; + + static final String JARS_PER_SECOND_KEY = "elastic.otel.sca.jars_per_second"; + static final String JARS_PER_SECOND_ENV = "ELASTIC_OTEL_SCA_JARS_PER_SECOND"; + + static final boolean DEFAULT_ENABLED = true; + static final boolean DEFAULT_SKIP_TEMP_JARS = true; + static final int DEFAULT_JARS_PER_SECOND = 10; + + private final boolean enabled; + private final boolean skipTempJars; + private final int jarsPerSecond; + + private SCAConfiguration(boolean enabled, boolean skipTempJars, int jarsPerSecond) { + this.enabled = enabled; + this.skipTempJars = skipTempJars; + this.jarsPerSecond = jarsPerSecond; + } + + /** Reads current configuration from system properties and environment variables. */ + static SCAConfiguration get() { + return new SCAConfiguration( + readBoolean(ENABLED_KEY, ENABLED_ENV, DEFAULT_ENABLED), + readBoolean(SKIP_TEMP_JARS_KEY, SKIP_TEMP_JARS_ENV, DEFAULT_SKIP_TEMP_JARS), + readInt(JARS_PER_SECOND_KEY, JARS_PER_SECOND_ENV, DEFAULT_JARS_PER_SECOND)); + } + + public boolean isEnabled() { + return enabled; + } + + public boolean isSkipTempJars() { + return skipTempJars; + } + + public int getJarsPerSecond() { + return jarsPerSecond; + } + + private static boolean readBoolean(String sysProp, String envVar, boolean defaultValue) { + String value = System.getProperty(sysProp); + if (value == null) { + value = System.getenv(envVar); + } + if (value == null) { + return defaultValue; + } + return "true".equalsIgnoreCase(value.trim()); + } + + private static int readInt(String sysProp, String envVar, int defaultValue) { + String value = System.getProperty(sysProp); + if (value == null) { + value = System.getenv(envVar); + } + if (value == null) { + return defaultValue; + } + try { + int parsed = Integer.parseInt(value.trim()); + return parsed > 0 ? parsed : defaultValue; + } catch (NumberFormatException e) { + return defaultValue; + } + } +} diff --git a/sca-extension/src/main/java/co/elastic/otel/sca/SCAExtension.java b/sca-extension/src/main/java/co/elastic/otel/sca/SCAExtension.java new file mode 100644 index 00000000..28a391d7 --- /dev/null +++ b/sca-extension/src/main/java/co/elastic/otel/sca/SCAExtension.java @@ -0,0 +1,146 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.otel.sca; + +import io.opentelemetry.javaagent.extension.AgentListener; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; +import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; +import java.lang.instrument.Instrumentation; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Entry point for the Elastic SCA extension. + * + *
Registered as both an {@link AutoConfigurationCustomizerProvider} and an {@link + * AgentListener} via two {@code META-INF/services/} files so that it participates in the OTel + * autoconfigure lifecycle at the correct phases: + * + *
Following the pattern of {@code inferred-spans} / {@code ElasticAutoConfigurationCustomizerProvider} + * + {@code ConfigLoggingAgentListener} in the {@code custom} module. + */ +public class SCAExtension implements AutoConfigurationCustomizerProvider, AgentListener { + + private static final Logger logger = Logger.getLogger(SCAExtension.class.getName()); + + // ---- AutoConfigurationCustomizerProvider -------------------------------- + + /** + * Registers default values for {@code elastic.otel.sca.*} properties so that OTel's config + * pipeline (system properties, env vars, SDK config file) can override them consistently. + * + *
Only sets a default when the user has not already supplied an explicit value, matching the
+ * pattern used by {@code InferredSpansBackwardsCompatibilityConfig}.
+ */
+ @Override
+ public void customize(AutoConfigurationCustomizer config) {
+ config.addPropertiesCustomizer(
+ props -> {
+ Map The JVM {@link Instrumentation} object is obtained via reflection from {@code
+ * io.opentelemetry.javaagent.bootstrap.InstrumentationHolder}, which is the same internal holder
+ * used by the upstream OTel Java agent. This class lives in the bootstrap classloader and is
+ * accessible from the agent classloader at runtime without a compile-time dependency.
+ */
+ @Override
+ public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk) {
+ SCAConfiguration config = SCAConfiguration.get();
+
+ if (!config.isEnabled()) {
+ logger.fine("SCA extension is disabled via " + SCAConfiguration.ENABLED_KEY);
+ return;
+ }
+
+ Instrumentation instrumentation = findInstrumentation();
+ if (instrumentation == null) {
+ logger.warning(
+ "SCA: could not obtain JVM Instrumentation object — JAR scanning will not run");
+ return;
+ }
+
+ JarCollectorService service =
+ new JarCollectorService(
+ autoConfiguredOpenTelemetrySdk.getOpenTelemetrySdk(), instrumentation, config);
+ service.start();
+ }
+
+ /**
+ * Retrieves the JVM {@link Instrumentation} instance from {@code
+ * io.opentelemetry.javaagent.bootstrap.InstrumentationHolder}.
+ *
+ * The OTel Java agent stores the {@code Instrumentation} it receives in {@code premain()} in
+ * this bootstrap-classloader class. Because our code runs in the agent classloader (which
+ * delegates to bootstrap), we can reach it via {@link Class#forName} without importing it at
+ * compile time — and without using {@code ByteBuddyAgent.getInstrumentation()}.
+ *
+ * This is the same mechanism used internally by the OTel contrib libraries (e.g.
+ * {@code opentelemetry-javaagent-inferred-spans}) to access the {@link Instrumentation}.
+ */
+ static Instrumentation findInstrumentation() {
+ try {
+ Class> holder =
+ Class.forName("io.opentelemetry.javaagent.bootstrap.InstrumentationHolder");
+ Method getter = holder.getMethod("getInstrumentation");
+ return (Instrumentation) getter.invoke(null);
+ } catch (Exception e) {
+ logger.log(
+ Level.WARNING,
+ "SCA: failed to obtain Instrumentation via InstrumentationHolder",
+ e);
+ return null;
+ }
+ }
+}
diff --git a/sca-extension/src/main/resources/META-INF/services/io.opentelemetry.javaagent.extension.AgentListener b/sca-extension/src/main/resources/META-INF/services/io.opentelemetry.javaagent.extension.AgentListener
new file mode 100644
index 00000000..5de17566
--- /dev/null
+++ b/sca-extension/src/main/resources/META-INF/services/io.opentelemetry.javaagent.extension.AgentListener
@@ -0,0 +1 @@
+co.elastic.otel.sca.SCAExtension
diff --git a/sca-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider b/sca-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider
new file mode 100644
index 00000000..5de17566
--- /dev/null
+++ b/sca-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider
@@ -0,0 +1 @@
+co.elastic.otel.sca.SCAExtension
diff --git a/sca-extension/src/test/java/co/elastic/otel/sca/JarMetadataExtractorTest.java b/sca-extension/src/test/java/co/elastic/otel/sca/JarMetadataExtractorTest.java
new file mode 100644
index 00000000..a65b99c9
--- /dev/null
+++ b/sca-extension/src/test/java/co/elastic/otel/sca/JarMetadataExtractorTest.java
@@ -0,0 +1,222 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package co.elastic.otel.sca;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Properties;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class JarMetadataExtractorTest {
+
+ @TempDir File tempDir;
+
+ // ---- pom.properties -------------------------------------------------------
+
+ @Test
+ void extractFromPomProperties() throws IOException {
+ File jar = buildJar("my-artifact-1.2.3.jar", jarOut -> {
+ addPomProperties(jarOut, "com.example", "my-artifact", "1.2.3");
+ });
+
+ JarMetadata meta = JarMetadataExtractor.extract(jar.getAbsolutePath(), "testloader");
+
+ assertThat(meta).isNotNull();
+ assertThat(meta.groupId).isEqualTo("com.example");
+ assertThat(meta.name).isEqualTo("my-artifact");
+ assertThat(meta.version).isEqualTo("1.2.3");
+ assertThat(meta.purl).isEqualTo("pkg:maven/com.example/my-artifact@1.2.3");
+ assertThat(meta.classloaderName).isEqualTo("testloader");
+ }
+
+ @Test
+ void pomPropertiesTakesPrecedenceOverManifest() throws IOException {
+ File jar = buildJar("artifact.jar", jarOut -> {
+ addPomProperties(jarOut, "com.pom", "pom-artifact", "2.0");
+ addManifest(jarOut, "Manifest-Artifact", "9.9.9", null);
+ });
+
+ JarMetadata meta = JarMetadataExtractor.extract(jar.getAbsolutePath(), "");
+
+ assertThat(meta.groupId).isEqualTo("com.pom");
+ assertThat(meta.name).isEqualTo("pom-artifact");
+ assertThat(meta.version).isEqualTo("2.0");
+ }
+
+ // ---- MANIFEST.MF ----------------------------------------------------------
+
+ @Test
+ void extractFromManifest() throws IOException {
+ File jar = buildJar("manifest-only.jar", jarOut -> {
+ addManifest(jarOut, "My Library", "3.1.0", null);
+ });
+
+ JarMetadata meta = JarMetadataExtractor.extract(jar.getAbsolutePath(), "");
+
+ assertThat(meta).isNotNull();
+ assertThat(meta.version).isEqualTo("3.1.0");
+ // name falls back to Implementation-Title when no artifactId
+ assertThat(meta.name).isEqualTo("My Library");
+ }
+
+ @Test
+ void manifestSpecificationVersionUsedWhenImplementationVersionAbsent() throws IOException {
+ File jar = buildJar("spec-version.jar", jarOut -> {
+ addManifest(jarOut, "SpecLib", null, "4.0");
+ });
+
+ JarMetadata meta = JarMetadataExtractor.extract(jar.getAbsolutePath(), "");
+
+ assertThat(meta).isNotNull();
+ assertThat(meta.version).isEqualTo("4.0");
+ }
+
+ // ---- Filename fallback ----------------------------------------------------
+
+ @Test
+ void extractVersionFromFilename() throws IOException {
+ File jar = buildJar("guava-32.1.3-jre.jar", jarOut -> { /* no metadata */ });
+
+ JarMetadata meta = JarMetadataExtractor.extract(jar.getAbsolutePath(), "");
+
+ assertThat(meta).isNotNull();
+ assertThat(meta.name).isEqualTo("guava");
+ assertThat(meta.version).isEqualTo("32.1.3-jre");
+ assertThat(meta.purl).isEqualTo("pkg:maven/guava@32.1.3-jre");
+ }
+
+ @Test
+ void noVersionInFilename() throws IOException {
+ File jar = buildJar("tools.jar", jarOut -> { /* no metadata */ });
+
+ JarMetadata meta = JarMetadataExtractor.extract(jar.getAbsolutePath(), "");
+
+ assertThat(meta).isNotNull();
+ assertThat(meta.name).isEqualTo("tools");
+ assertThat(meta.version).isEmpty();
+ }
+
+ // ---- pURL construction ----------------------------------------------------
+
+ @Test
+ void purlWithoutGroupId() {
+ assertThat(JarMetadataExtractor.buildPurl("", "my-lib", "1.0"))
+ .isEqualTo("pkg:maven/my-lib@1.0");
+ }
+
+ @Test
+ void purlWithoutVersion() {
+ assertThat(JarMetadataExtractor.buildPurl("org.example", "lib", ""))
+ .isEqualTo("pkg:maven/org.example/lib");
+ }
+
+ @Test
+ void purlEmptyWhenNoArtifactId() {
+ assertThat(JarMetadataExtractor.buildPurl("org.example", "", "1.0")).isEmpty();
+ }
+
+ // ---- SHA-256 --------------------------------------------------------------
+
+ @Test
+ void sha256IsDeterministic() throws IOException {
+ File jar = buildJar("stable.jar", jarOut -> {
+ addPomProperties(jarOut, "org.stable", "stable", "1.0");
+ });
+
+ String first = JarMetadataExtractor.computeSha256(jar);
+ String second = JarMetadataExtractor.computeSha256(jar);
+
+ assertThat(first).isNotEmpty().hasSize(64).isEqualTo(second);
+ }
+
+ @Test
+ void sha256DiffersForDifferentContent() throws IOException {
+ File jar1 = buildJar("a.jar", jarOut -> addPomProperties(jarOut, "g", "a", "1"));
+ File jar2 = buildJar("b.jar", jarOut -> addPomProperties(jarOut, "g", "b", "2"));
+
+ assertThat(JarMetadataExtractor.computeSha256(jar1))
+ .isNotEqualTo(JarMetadataExtractor.computeSha256(jar2));
+ }
+
+ // ---- Missing file ---------------------------------------------------------
+
+ @Test
+ void returnsNullForNonExistentFile() {
+ JarMetadata meta = JarMetadataExtractor.extract("/does/not/exist.jar", "");
+ assertThat(meta).isNull();
+ }
+
+ // ---- Helpers --------------------------------------------------------------
+
+ @FunctionalInterface
+ interface JarPopulator {
+ void populate(JarOutputStream jarOut) throws IOException;
+ }
+
+ private File buildJar(String name, JarPopulator populator) throws IOException {
+ File jar = new File(tempDir, name);
+ try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jar))) {
+ populator.populate(jos);
+ }
+ return jar;
+ }
+
+ private static void addPomProperties(
+ JarOutputStream jos, String groupId, String artifactId, String version) throws IOException {
+ jos.putNextEntry(
+ new JarEntry("META-INF/maven/" + groupId + "/" + artifactId + "/pom.properties"));
+ Properties props = new Properties();
+ props.setProperty("groupId", groupId);
+ props.setProperty("artifactId", artifactId);
+ props.setProperty("version", version);
+ props.store(jos, null);
+ jos.closeEntry();
+ }
+
+ private static void addManifest(
+ JarOutputStream jos,
+ String implTitle,
+ String implVersion,
+ String specVersion) throws IOException {
+ Manifest mf = new Manifest();
+ Attributes attrs = mf.getMainAttributes();
+ attrs.put(Attributes.Name.MANIFEST_VERSION, "1.0");
+ if (implTitle != null) {
+ attrs.put(Attributes.Name.IMPLEMENTATION_TITLE, implTitle);
+ }
+ if (implVersion != null) {
+ attrs.put(Attributes.Name.IMPLEMENTATION_VERSION, implVersion);
+ }
+ if (specVersion != null) {
+ attrs.put(Attributes.Name.SPECIFICATION_VERSION, specVersion);
+ }
+ jos.putNextEntry(new JarEntry(JarFile.MANIFEST_NAME));
+ mf.write(jos);
+ jos.closeEntry();
+ }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 02d8cc39..6a793563 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -19,6 +19,7 @@ include("custom")
include("instrumentation")
include("instrumentation:openai-client-instrumentation:instrumentation-1.1")
include("inferred-spans")
+include("sca-extension")
include("internal-logging")
include("resources")
include("runtime-attach")